如何透過ForkJoinPool和HikariCP將大型JSON檔案批次處理到MySQL?

banq發表於2019-02-24

這是一個Spring Boot應用程式展示案例,它讀取一個相對較大的JSON檔案(200000多行),並使用ForkJoinPoolAPI和HikariCP 透過批處理將其內容插入MySQL 。

關鍵點:
1.  使用MySQL  json型別

-- Create the table 
CREATE TABLE `lots` (
  `lot` json DEFAULT NULL
);


2. 對於MySQL,application.properties您可能希望將以下內容附加到JDBC URL:
  • rewriteBatchedStatements=true: 此設定將強制在單個請求中傳送批處理語句;
  • cachePrepStmts=true:如果您決定設定啟用快取和是非常有用的  prepStmtCacheSize,prepStmtCacheSqlLimit等為好; 沒有此設定,快取被禁用;
  • useServerPrepStmts=true:這樣你就可以切換到伺服器端準備好的語句(可能會帶來顯著的效能提升); 此外,您可以避免PreparedStatement在JDBC驅動程式級別進行模擬;
  • 我們使用以下JDBC URL設定: ...?cachePrepStmts=true&useServerPrepStmts=true&rewriteBatchedStatements=true&createDatabaseIfNotExist=true


application.properties:

spring.datasource.url=jdbc:mysql://localhost:3306/citylots_db?cachePrepStmts=true&useServerPrepStmts=true&rewriteBatchedStatements=true&createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect

spring.jpa.properties.hibernate.jdbc.batch_size = 300

spring.datasource.initialization-mode=always
spring.datasource.platform=mysql

spring.datasource.hikari.maximumPoolSize=8
spring.datasource.hikari.minimumIdle=8

logging.level.com.zaxxer.hikari.HikariConfig=DEBUG
logging.level.com.zaxxer.hikari=TRACE


注意:  較舊的MySQL版本不能容忍將重寫和伺服器端預處理語句一起啟用。為確保這些陳述仍然有效,請檢視您正在使用的Connector / J的註釋。
  • 設定HikariCP以提供許多資料庫連線,以確保資料庫實現最小上下文切換(例如,2 * CPU核心數)
  • 此應用程式用於  StopWatch測量將檔案傳輸到資料庫所需的時間
  • 要執行應用程式,您必須citylots.zip在當前位置解壓縮; 這是從Internet收集的相對較大的JSON檔案
  • 如果要檢視有關批處理的詳細資訊,只需啟用  DatasourceProxyBeanPostProcessor.java元件,取消註釋  @Component; 這是必需的,因為此應用程式依賴於DataSource-Proxy


3. 將JSON檔案讀入一定容量的List,例如,等於或大於批處理資料大小; 預設情況下,批次大小為300行,臨時列表為300 * 64(建議不要用這些值隨意進行實驗!)


@Repository
@Transactional
@Scope("prototype")
public class RecursiveActionRepository extends RecursiveAction {

    @Value("${spring.jpa.properties.hibernate.jdbc.batch_size}")
    private int batchSize;

    @PersistenceContext
    private EntityManager entityManager;
    
    @Autowired
    private ApplicationContext applicationContext;

    private final List<String> jsonList;

    private static final Logger logger = Logger.getLogger(RecursiveActionRepository.class.getName());
    private static final String SQL_INSERT = "INSERT INTO lots (lot) VALUES (?)";

    public RecursiveActionRepository(List<String> jsonList) {
        this.jsonList = jsonList;
    }   

    @Override
    public void compute() {
        if (jsonList.size() > batchSize) {
            ForkJoinTask.invokeAll(createSubtasks());
        } else {
            Session hibernateSession = entityManager.unwrap(Session.class);
            hibernateSession.doWork(this::insertJson);
        }
    }

    private List<RecursiveActionRepository> createSubtasks() {
        List<RecursiveActionRepository> subtasks = new ArrayList<>();

        int size = jsonList.size();

        List<String> jsonListOne = jsonList.subList(0, (size + 1) / 2);
        List<String> jsonListTwo = jsonList.subList((size + 1) / 2, size);

        subtasks.add(applicationContext.getBean(
                RecursiveActionRepository.class, new ArrayList<>(jsonListOne)));
        subtasks.add(applicationContext.getBean(
                RecursiveActionRepository.class, new ArrayList<>(jsonListTwo)));

        return subtasks;
    }

    public void insertJson(Connection connection) {
        try (PreparedStatement preparedStatement = connection.prepareStatement(SQL_INSERT)) {

            int i = 1;
            for (String jsonLine : jsonList) {
                preparedStatement.setString(1, jsonLine);
                preparedStatement.addBatch();

                if (i % batchSize == 0) {
                    preparedStatement.executeBatch();
                    i = 0;
                }

                i++;
            }

            if (i > 1) {
                preparedStatement.executeBatch();
            }
            
            logger.log(Level.INFO, "Processed by {0}", Thread.currentThread().getName());

        } catch (SQLException e) {
            logger.log(Level.SEVERE, "SQL exception", e);
        }
    }
}


4. 列表減半並建立子任務見createSubtasks,直到列表大小小於批次大小(例如,預設小於300)
當列表已滿時,將其分批儲存到MySQL中,清除列表,然後重新填充

原始碼可以在這裡找到。

相關文章