首頁 > 軟體

java之CSV大批次資料入庫的實現

2023-02-06 06:02:34

需求

讀200+的CSV/EXCEL檔案,按檔名稱存到不同資料庫

前期準備

環境

maven + jdk8 + mysql

程式碼展示

pom檔案

    <dependencies>
        <!--https://mvnrepository.com/artifact/com.opencsv/opencsv -->
        <dependency>            //讀取csv檔案 
            <groupId>com.opencsv</groupId>
            <artifactId>opencsv</artifactId>
            <version>${opencsv.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--mybatis-plus 持久層-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <!-- velocity 模板引擎, Mybatis Plus 程式碼生成器需要 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>${velocity.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

關鍵程式碼及思路

多執行緒處理資料,否則8k萬資料太慢了

建立執行緒池,個數的大小一般取決於自己電腦設定,以及I/O還是CPU密集型。

  • I/O密集型:CPU*2 + 1
  • CPU密集型:CPU + 1
  • Java檢視CPU數目
public static final Integer N_Thread = Runtime.getRuntime().availableProcessors() * 2 + 1;
 ExecutorService service = Executors.newFixedThreadPool(N_Thread );
 List<CompletableFuture<Void>> futureList = new ArrayList<>();

資料進行分批次處理,如果資料一次過大,可能導致sql拼接失敗或者程式連線超時的問題。

            List<AirStationHourEntity> records = new ArrayList<>(table.values());
            int basic = 0, total = records.size();

            do{
                int finalBasic = basic;
                String finalTableName = tableName;
                CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
                    List<AirStationHourEntity> subRecords = records.subList(finalBasic * 6000, Math.min((1 + finalBasic) * 6000, total));
                    airStationHourMapper.saveOrUpdateBatch(finalTableName, subRecords);
                }, service);
                // 加入執行緒集合,方便後續阻塞主執行緒,防止執行緒沒跑就提前結束
                futureList.add(completableFuture);
                basic++;
            }while (basic * 6000 < total);

獲取檔案資料

		String url = dataConfig.getStation();
        List<String> stationFiles = FileUtils.findFiles(url);
        //遍歷檔案
        for (String fileName : stationFiles) {
            try {
                // 讀取檔案資料
                String path = url.concat(fileName);
                CSVReader reader = new CSVReader(new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8));
                List<String[]> lines = reader.readAll();

根據檔名建立相應資料庫

 tableName = fileName.substring(fileName.lastIndexOf("_") + 1, fileName.indexOf(".") - 2);
                tableName = TABLE_PREFIX.STATION_PREFIX.value.concat(tableName);
                if (airStationHourMapper.findTableByName(tableName) == 0) {
                    airStationHourMapper.createNewTable(tableName);
                }

注意:如果在主執行緒完成之後,沒有對主執行緒進行阻塞,會導致執行緒池中的執行緒沒跑完就直接結束了,需要遍歷執行緒集合來阻塞主執行緒

 for (CompletableFuture<Void> future : futureList) {
     future.join();
 }

關鍵的SQL語句

saveOrUpdateBatch

這裡選用Mysql提供的ON DUPLICATE KEY UPDATE來實現更新或者插入,如果primary key 或者 unique key不存在就插入,否則就更新。注意:primary key 和 unique key都存在的時候可能會導致資料的更新的異常,這裡建議選其中一個最為鍵,否則容易死鎖!見方案選型

 <update id="saveOrUpdateBatch">
        insert into ${tableName}
        (
        <include refid="Base_Column_List"/>
        )
        values
        <foreach collection="records" item="record" separator=",">
            (
            #{record.dataId,jdbcType=VARCHAR},
            #{record.stationId,jdbcType=VARCHAR},
            #{record.dataTime,jdbcType=TIMESTAMP},
            #{record.aqi,jdbcType=INTEGER},
            #{record.priPol,jdbcType=VARCHAR},
            #{record.co,jdbcType=DOUBLE},
            #{record.co24,jdbcType=DOUBLE},
            #{record.no2,jdbcType=INTEGER},
            #{record.no224,jdbcType=INTEGER},
            #{record.so2,jdbcType=INTEGER},
            #{record.so224,jdbcType=INTEGER},
            #{record.o3,jdbcType=INTEGER},
            #{record.o324,jdbcType=INTEGER},
            #{record.o38,jdbcType=INTEGER},
            #{record.o3824,jdbcType=INTEGER},
            #{record.pm10,jdbcType=INTEGER},
            #{record.pm1024,jdbcType=INTEGER},
            #{record.pm25,jdbcType=INTEGER},
            #{record.pm2524,jdbcType=INTEGER}
            )
        </foreach>
        ON DUPLICATE KEY UPDATE
        AQI = VALUES(AQI),pri_pol = VALUES(pri_pol),
        CO = VALUES(CO),CO_24 = VALUES(CO_24),
        NO2 = VALUES(NO2),NO2_24 = VALUES(NO2_24),
        SO2 = VALUES(SO2),SO2_24 = VALUES(SO2_24),
        O3 = VALUES(O3),O3_8 = VALUES(O3_8),O3_8_24 = VALUES(O3_8_24),
        PM10 = VALUES(PM10),PM10_24 = VALUES(PM10_24),
        PM2_5 = VALUES(PM2_5),PM2_5_24 =VALUES(PM2_5_24);
    </update>

方案選型

此方案時間過久,10多個小時大概能完成100w+的資料;

在程式中查詢資料庫中現存的資料,然後對這些資料進行更新,其餘的進行插入

此方案程式程式碼看起來較繁瑣。

選用 ON DUPLICATE KEY UPDATE & 多執行緒來實現批次處理。 問題 在處理最後一些資料時,報異常:獲取不到資料庫連線,連線超時

解決方案

修改application.yml的sql設定

    hikari:
      connection-timeout: 600000  //時間設的長一些

當即存在primary key 以及 unique key時,出現了死鎖

如果一個表定義有多個唯一鍵(包括唯一索引、主鍵)時,是不安全的。
當mysql執行INSERT ON DUPLICATE KEY的INSERT時,儲存引擎會檢查插入的行為是否產生重複錯誤。

org.springframework.dao.DeadlockLoserDataAccessException:
### Error updating database.  Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
### The error may exist in class path resource [mapper/PersonGroupRefMapper.xml]
### The error may involve com.order.addOrder-Inline
### The error occurred while setting parameters
### SQL: insert into t_***(XX,XX,XX,XX,XX,XX) values         (?, ?, ?, ?, ?, ?)         ON DUPLICATE KEY UPDATE XX= VALUES(XX), XX= VALUES(XX), XX= VALUES(XX)
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction Deadlock found when trying to get lock; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transactio

解決方案

見上面關鍵SQL,刪除了primary key 留下了unique key重新建表。

見上面關鍵SQL,刪除了primary key 留下了unique key重新建表。

批次資料入庫,當SQL語句拼接過長,超過了設定的最大的限制。

解決方案

批次資料入庫時,稍微減少資料量再進行插入,如6000條資料減為4000資料之後再批次入庫。

執行結果

相比之前一天200W+資料有了質的提升,半小時完成了所有資料的預處理以及入庫。

到此這篇關於java之CSV大批次資料入庫的實現的文章就介紹到這了,更多相關java CSV大批次入庫內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com! 


IT145.com E-mail:sddin#qq.com