天天看點

解析csv檔案,讀取百萬級資料

本文正在參加「金石計劃 . 瓜分6萬現金大獎」

最近在處理下載下傳支付寶賬單的需求,支付寶都有代碼示例,功能完成還是比較簡單的,唯一的問題就在于下載下傳後的檔案資料讀取。賬單檔案可大可小,要保證其可用以及性能就不能簡單粗暴的完成開發就行。

檔案下載下傳是是csv格式,此檔案按照行讀取,每一行中各列資料直接用逗号,隔開的。

前置設定:

  1. 開啟了設定記憶體大小以及GC日志輸出配置-Xms800m -Xmx800m -XX:+PrintGCDetails
  2. 測試檔案total-file.csv資料量: 100萬,檔案大小:176M
  3. 定義賬單檔案的屬性字段:
private static final List<String> ALI_FINANCE_LIST = new ArrayList<>(
                Arrays.asList("FINANCE_FLOW_NUMBER", "BUSINESS_FLOW_NUMBER", "MERCHANT_ORDER_NUMBER", "ITEM_NAME", "CREATION_TIME", "OPPOSITE_ACCOUNT", "RECEIPT_AMOUNT", "PAYMENT_AMOUNT", "ACCOUNT_BALANCE", "BUSINESS_CHANNEL", "BUSINESS_TYPE", "REMARK"));
複制代碼           

相關推薦閱讀:

圖形化監控工具JConsole

虛拟機的日志和日志參數

第一版:簡單粗暴

直來直往,毫無技巧

拿到檔案流,直接按行讀取,把所有的資料放入到List<Map<String, Object>>中(其中業務相關的校驗以及資料篩選都去掉了)

代碼如下

@ApiOperation(value = "測試解析-簡單粗暴版")
    @GetMapping("/readFileV1")
    public ResponseEntity readFileV1(){
        File file = new File("/Users/ajisun/projects/alwaysCoding/files/total-file.csv");
        List<Map<String, Object>> context = new ArrayList<>();
        try (
                InputStream stream = new FileInputStream(file);
                InputStreamReader isr = new InputStreamReader(stream, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr)
        ) {
            String line = "";
            int number = 1;
            while ((line = br.readLine()) != null) {
                //去除#号開始的行
                if (!line.startsWith("#")) {
                    if (number >= 1) {
                        //csv是以逗号為區分的檔案,以逗号區分
                        String[] columns = line.split(",", -1);
                        //建構資料
                        Map<String, Object> dataMap = new HashMap<>(16);
                        for (int i = 0; i < columns.length; i++) {
                            //防止異常,大于預定義的列不處理
                            if (i > ALI_FINANCE_LIST.size()) {
                                break;
                            }
                            dataMap.put(ALI_FINANCE_LIST.get(i), columns[i].trim());
                        }
                        context.add(dataMap);
                    }
                    number++;
                }
            }
            // TODO 存表
            System.out.println("=====插入資料庫,資料條數:"+context.size());
            System.out.println("對象大小:"+(ObjectSizeCalculator.getObjectSize(context)/1048576) +" M");
            context.clear();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
複制代碼           

輸出日志以及Jconsole的監控如下

解析csv檔案,讀取百萬級資料
解析csv檔案,讀取百萬級資料
由上面的圖可以看出記憶體和CPU的使用率都比較高,會不斷觸發Full GC,最終還出現了OOM,記憶體基本使用完了,cpu使用也達到了近70%。

去除-Xms800m -Xmx800m的記憶體大小限制後可以把全部資料拿到,結果如下圖所示

解析csv檔案,讀取百萬級資料
解析csv檔案,讀取百萬級資料
所有資料可以正常解析讀取,Full GC也沒用前一次頻繁,沒有出現OOM。10w條資料大小有1.2G,所占用的記憶體更是達到2.5G,CPU也是近60%的使用率。

僅僅是200M的csv檔案,堆記憶體就占用了2.5G,如果是更大的檔案,記憶體占用不得起飛了

嚴重占用了系統資源,對于大檔案,此方法不可取。

第二版:循序漸進

緩緩圖之,資料分批

第一版記憶體、CPU占用過大,甚至OOM,主要原因就是把所有資料全部加載到記憶體了。為了避免這種情況,我們可以分批處理。

參數說明:

  • file:解析的檔案
  • batchNumOrder:批次号
  • context:存放資料的集合
  • count:每一批次的資料量

1. 接口API

@ApiOperation(value = "測試解析-資料分批版")
    @GetMapping("/readFileV2")
    public ResponseEntity readFileV2(@RequestParam(required = false) int count) {
        File file = new File("/Users/ajisun/projects/alwaysCoding/files/total-file.csv");
        List<Map<String, Object>> context = new ArrayList<>();
        int batchNumOrder = 1;
        parseFile(file, batchNumOrder, context, count);
        return null;
    }
複制代碼           

2. 檔案解析

檔案解析,擷取檔案流
private int parseFile(File file, int batchNumOrder, List<Map<String, Object>> context, int count) {
        try (
                InputStreamReader isr = new InputStreamReader(new FileInputStream(file) , StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr)
        ) {
            batchNumOrder = this.readDataFromFile(br, context, batchNumOrder, count);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return batchNumOrder;
    }
複制代碼           

3. 讀取檔案資料

按行讀取檔案,分割每行資料,然後按照#{count}的數量拆分,分批次存儲
private int readDataFromFile(BufferedReader br, List<Map<String, Object>> context, int batchNumOrder, int count) throws IOException {
        String line = "";
        int number = 1;
        while ((line = br.readLine()) != null) {
            //去除#号開始的行
            if (!line.startsWith("#")) {
                if (number >= 1) {
                    //csv是以逗号為區分的檔案,以逗号區分
                    String[] columns = line.split(",", -1);
                    //建構資料
                    context.add(constructDataMap(columns));
                }
                number++;
            }

            if (context.size() >= count) {
                // TODO 存表
                System.out.println("=====插入資料庫:批次:" + batchNumOrder + ",資料條數:" + context.size());
                context.clear();
                batchNumOrder++;
            }
        }
        // 最後一批次送出
        if (CollectionUtils.isNotEmpty(context)) {
            System.out.println("=====插入資料庫:批次:" + batchNumOrder + ",資料條數:" + context.size());
            context.clear();
        }
        return batchNumOrder;
    }
複制代碼           

4.組裝資料

把每一行資料按照順序和業務對象ALI_FINANCE_LIST比對 ,組裝成功單個map資料
public Map<String, Object> constructDataMap(String[] columns) {
        Map<String, Object> dataMap = new HashMap<>(16);
        for (int i = 0; i < columns.length; i++) {
            //防止異常,大于預定義的列不處理
            if (i > ALI_FINANCE_LIST.size()) {
                break;
            }
            dataMap.put(ALI_FINANCE_LIST.get(i), columns[i].trim());
        }
        return dataMap;
    }
複制代碼           

5.執行結果

解析csv檔案,讀取百萬級資料
解析csv檔案,讀取百萬級資料

把檔案分批讀取插入資料庫,可以減少記憶體的占用以及解決高CPU的問題。已經可以很好的處理檔案讀取問題了。

但是如果一個檔案更大,有1G,2G 甚至更大,雖然不會造成OOM ,但是整個解析的時間就會比較長,然後如果中間出現問題,那麼就需要從頭再來。

假如是1000萬資料的檔案,按照一批次1萬條插入資料庫,然而到999批次的時候失敗了(不考慮復原),那麼為了保證資料的完整性,該檔案就需要重新上傳解析。但實際上隻需要最後一批次資料即可, 多了很多重複操作。

可以使用另一種方式處理,第三版

第三版:大而化小

分而治之,檔案拆分

主要改動就是在第二版的基礎增加檔案拆分的功能,把一個大檔案按照需求拆分成n個小檔案,然後單獨解析拆分後的小檔案即可。其他方法不變。

1.接口API

擷取拆分後的檔案,循環解析讀取
@ApiOperation(value = "測試解析-檔案拆分版")
    @GetMapping("/readFileV3")
    public ResponseEntity readFileV3(@RequestParam(required = false) int count){
        if (StringUtils.isEmpty(date)) {
            this.execCmd();
        }
        File file = new File("/Users/ajisun/projects/alwaysCoding/files");
        File[] childs = file.listFiles();//可以按照需求自行排序
        for (File file1 : childs) {
            if (!file1.getName().contains(".csv") && file1.getName().contains("total-file-")) {
                file1.renameTo(new File(file1.getAbsolutePath() + ".csv"));
            }
        }
        int batchNumOrder = 1;
        List<Map<String, Object>> context = new ArrayList<>();
        for (File child : childs) {
             if (!child.getName().contains("total-file-")){
                 continue;
             }
            batchNumOrder = parseFile(child, batchNumOrder, context, count);
        }
        return null;
    }
複制代碼           

2.檔案拆分

按照需求使用Linux指令拆分檔案,大而化小,然後按照一定規則命名
public List<String> execCmd() {
        List<String> msgList = new ArrayList<String>();
        String command = "cd /Users/ajisun/projects/alwaysCoding/files && split -a 2 -l 10000  total-file.csv  total-file-";
        try {
            ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", command);
            Process process = pb.start();
            BufferedReader ir = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = ir.readLine()) != null) {
                msgList.add(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(msgList);
        return msgList;
    }
複制代碼           

這種方式的處理在記憶體與CPU的占用和第二版基本沒有差别。

如果采用這種方式記得檔案的清理,避免磁盤空間的占用

技術擴充:檔案拆分

cd /Users/ajisun/projects/alwaysCoding/files && split -a 2 -l 10000 total-file.csv total-file-
複制代碼           

上述字元串是兩個指令用&&連接配接,第一個是進入到指定檔案夾,第二個就是按照10000行拆分total-file.csv,而且子檔案命名以total-file-開頭,字尾預設兩位字母結尾. 執行後的結果如下圖

mac下不能用數字命名(linux下可以的),隻能是預設的字母命名

Linux下:ajisun.log檔案按照檔案大小50m切割,字尾是2位數字結尾的子檔案,子檔案以ajisun-開頭

解析csv檔案,讀取百萬級資料

總結總結

如果确定了解析的檔案都是小檔案,而且檔案中的資料最多也就幾萬行,那麼直接簡單粗暴使用第一版也沒問題。

如果檔案較大,幾十兆,或者檔案中的資料有大幾十萬行,那麼就使用第二版的分批處理。

如果檔案很大,以G為機關,或者檔案中的資料有幾百萬行,那麼就使用第三版的檔案拆分

這裡隻是做檔案解析以及讀取相關的功能,但是在實際情況中可能會存在各種各樣的資料校驗,這個需要根據自己的實際情況處理,但是要避免在解析大檔案的時候循環校驗,以及循環操作資料庫。必要時還可以引入中間表存儲檔案資料(不做任何處理),在中間表中做資料校驗 再同步到目标表。

還有沒有其他更好,更優的方式,歡迎評論區讨論

繼續閱讀