java導出超大 excel 文件

結(jié)論

通過 POI的SXSSFWorkbook定庵,使用操作系統(tǒng)的臨時文件來作為緩存吏饿,可以生成超大的excel 文件(我自己測試到500W,就沒往下測了)。

記得使用壓縮蔬浙。關鍵代碼

SXSSFWorkbook wb = null;
try {
    wb = new SXSSFWorkbook();
    wb.setCompressTempFiles(true); //壓縮臨時文件猪落,很重要,否則磁盤很快就會被寫滿
    ...
} finally {
    if (wb != null) {
        wb.dispose();// 刪除臨時文件畴博,很重要笨忌,否則磁盤可能會被寫滿
    }
}

背景

由于業(yè)務需要,最近要做一個導出超大數(shù)據(jù)的功能俱病。之間已經(jīng)有人做過一版官疲,由于受到POI 導出超大數(shù)據(jù)量時會出錯的影響,它把一個大文件拆成很多個小文件亮隙,然后再壓縮下載途凫,結(jié)果經(jīng)常出現(xiàn)少一兩個文件的問題。

目標

支持單個 excel 的 sheet 導出100w 的數(shù)據(jù)

方案

導出 csv 文件

首先想到的是導出 csv 文件溢吻,最方便维费。但是調(diào)研后,也是最快放棄的,因為它存在兩個很嚴重的問題:

  • 不同系統(tǒng)上的編碼不一樣犀盟,需要人工選擇而晒,對于普通用戶不做好
  • 沒有優(yōu)化和數(shù)據(jù)壓縮,數(shù)據(jù)量越大阅畴,csv 文件的大小比 excel 更大倡怎,當數(shù)據(jù)導出超過10w 時,csv 文件大小是 excel 的1.5倍
導出格式 1w 10w 30w 50w 70w 90w 100w
csv 4.0K/120ms 50M/1261ms 160M/3828ms 271M/7415ms 381M/8929ms 491M/11356ms 546M/13688ms

每行30個字段,每個字段里的內(nèi)容由 Math.random()產(chǎn)生

導出 excel 文件

大數(shù)據(jù)量的情況下贱枣,csv 的表現(xiàn)較差诈胜。只能考慮 excel. 對 excel 作了一個簡單的測試

指標 1w 2w 3w 4w 5w 6w 7w 8w 10w
耗時 3326ms 6483ms 7894 ms 9899 ms 12873 ms 15198 ms 17362 ms 20106 ms 25494 ms
導出文件大小 3.7M 7.4MM 12M 15M 19M 23M 26M 30M 37M
cpu 使用率 100% 100% 100% 100% 100% 200% 200% 800% 900%

cpu 使用率均指穩(wěn)定時的 cpu 使用率

發(fā)現(xiàn)幾個很嚴重的問題:

  • 隨著數(shù)據(jù)量的增大,cpu使用率直線上升冯事,這會給系統(tǒng)帶來很大的風險
  • 當數(shù)據(jù)量超過10w 時焦匈,會出現(xiàn) OOM 異常

excel 在內(nèi)存里存儲地越來越大,研究到了瓶頸昵仅。要解決這個問題缓熟,有兩種方案:

  • 先生成多個小 execel 文件,最后合并成一個大文件摔笤。查了文檔够滑,發(fā)現(xiàn)Java 里的工具都是先讀出來,再寫到 Workbook 對象里, 這樣還是會碰到同樣的問題吕世。如果用 excel 的工具彰触,則運維成本過大,因此這個方案行不通
  • 參考操作系統(tǒng)里的虛擬內(nèi)存命辖,用這個來突破 機器的內(nèi)存限制况毅。但是磁盤的性能很差,這樣做的效率很低尔艇。

這時尔许,在 POI 的文檔里發(fā)現(xiàn)了SXSSFWorkbook,其支持使用臨時文件终娃,可以用來生成超大 Excel 文件味廊。

Since 3.8-beta3, POI provides a low-memory footprint SXSSF API built on top of XSSF.

SXSSF is an API-compatible streaming extension of XSSF to be used when very large
 spreadsheets have to be produced, and heap space is limited. SXSSF achieves its
 low memory footprint by limiting access to the rows that are within a sliding window,
 while XSSF gives access to all rows in the document. Older rows that are no longer
 in the window become inaccessible, as they are written to the disk.

In auto-flush mode the size of the access window can be specified, to hold a certain
number of rows in memory. When that value is reached, the creation of an additional
row causes the row with the lowest index to to be removed from the access window and
written to disk. Or, the window size can be set to grow dynamically; it can be trimmed
periodically by an explicit call to flushRows(int keepRows) as needed.

Due to the streaming nature of the implementation, there are the following
limitations when compared to XSSF:
 * Only a limited number of rows are accessible at a point in time.
 * Sheet.clone() is not supported.
 * Formula evaluation is not supported

以下是 SXSSFWorkbook的測試結(jié)果:

使用緩存文件導出 excel

指標 10w 20w 30w 50w 80w 100w 150w 200w 300w
導出文件大小 37M 74M 111M 184M 295M 368M 552M 736M 1.1G
耗時(ms) 16259 29516 45846 75503 120434 156484 233730 303510 463399
cpu 使用率 100 100 100 100 100 100 100 100 100
內(nèi)存使用(k) 149460 176576 141940 143700 168460 180168 169632 198320 187484
緩存文件大小 37M 74M 111M 185M 295M 369M 553M 737M 1.1G

可以看到,其在性能與資源耗用上都比較平均棠耕,至此余佛,問題完美解決。

SXSSFWorkbook在使用上有一些注意項

  • Note that SXSSF allocates temporary files that you must always clean up explicitly, by calling the dispose method.
  • ?
SXSSF flushes sheet data in temporary files (a temp file per sheet) and the size
of these temporary files can grow to a very large value. For example, for a 20 MB
csv data the size of the temp xml becomes more than a gigabyte. If the size of the
 temp files is an issue, you can tell SXSSF to use gzip compression:

  SXSSFWorkbook wb = new SXSSFWorkbook();
  wb.setCompressTempFiles(true); // temp files will be gzipped

測試代碼

生成 csv

    private static void prcoessCSV(int rowsNum) throws Exception {
        try {
            long startTime = System.currentTimeMillis();
            final int NUM_OF_ROWS = rowsNum;
            final int NUM_OF_COLUMNS = 30;


            File file = new File("ooxml-scatter-chart_" + rowsNum + ".csv");
            BufferedWriter bf = new BufferedWriter(new FileWriter(file));
            StringBuffer sb = new StringBuffer();
            try {
                for (int rownum = 0; rownum < NUM_OF_ROWS; rownum++) {
                    for (int cellnum = 0; cellnum < NUM_OF_COLUMNS; cellnum++) {
                        sb.append(Math.random());
                        if ((cellnum + 1) != NUM_OF_COLUMNS) {
                            sb.append(",");
                        }
                    }
                    sb.append("\n");
                    if (rownum % 10000 == 0) {
                        bf.write(sb.toString());
                        sb = new StringBuffer();
                    }
                }
                bf.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            }

            long endTime = System.currentTimeMillis();
            System.out.println("process " + rowsNum + " spent time:" + (endTime - startTime));

        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        }
    }

excel窍荧,不使用緩存

       try {
            long startTime = System.currentTimeMillis();
            final int NUM_OF_ROWS = rowsNum;
            final int NUM_OF_COLUMNS = 30;


            Workbook wb = new XSSFWorkbook();
            Sheet sheet = wb.createSheet("Sheet 1");
            // Create a row and put some cells in it. Rows are 0 based.
            Row row;
            Cell cell;
            for (int rowIndex = 0; rowIndex < NUM_OF_ROWS; rowIndex++) {
                row = sheet.createRow(rowIndex);
                for (int colIndex = 0; colIndex < NUM_OF_COLUMNS; colIndex++) {
                    cell = row.createCell(colIndex);
                    cell.setCellValue(Math.random());
                }
            }

            // Write the output to a file
            FileOutputStream out = new FileOutputStream("ooxml-scatter-chart_XSSF_" + rowsNum + ".xlsx");
            wb.write(out);
            out.close();
            wb.close();

            long endTime = System.currentTimeMillis();
            System.out.println("process " + rowsNum + " spent time:" + (endTime - startTime));

        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        }

excel,使用緩存

       try {
            long startTime = System.currentTimeMillis();
            final int NUM_OF_ROWS = rowsNum;
            final int NUM_OF_COLUMNS = 30;

            SXSSFWorkbook wb = null;
            try {
                wb = new SXSSFWorkbook();
                wb.setCompressTempFiles(true); //壓縮臨時文件辉巡,很重要,否則磁盤很快就會被寫滿
                Sheet sh = wb.createSheet();
                int rowNum = 0;
                for (int num = 0; num < NUM_OF_ROWS; num++) {
                    if (num % 100_0000 == 0) {
                        sh = wb.createSheet("sheet " + num);
                        rowNum = 0;
                    }
                    rowNum++;
                    Row row = sh.createRow(rowNum);
                    for (int cellnum = 0; cellnum < NUM_OF_COLUMNS; cellnum++) {
                        Cell cell = row.createCell(cellnum);
                        cell.setCellValue(Math.random());
                    }
                }

                FileOutputStream out = new FileOutputStream("ooxml-scatter-chart_SXSSFW_" + rowsNum + ".xlsx");
                wb.write(out);
                out.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                if (wb != null) {
                    wb.dispose();// 刪除臨時文件搅荞,很重要红氯,否則磁盤可能會被寫滿
                }
            }

            long endTime = System.currentTimeMillis();
            System.out.println("process " + rowsNum + " spent time:" + (endTime - startTime));
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末框咙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子痢甘,更是在濱河造成了極大的恐慌喇嘱,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件塞栅,死亡現(xiàn)場離奇詭異者铜,居然都是意外死亡,警方通過查閱死者的電腦和手機放椰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門作烟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人砾医,你說我怎么就攤上這事拿撩。” “怎么了如蚜?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵压恒,是天一觀的道長。 經(jīng)常有香客問我错邦,道長探赫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任撬呢,我火速辦了婚禮伦吠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘魂拦。我一直安慰自己毛仪,他們只是感情好,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布晨另。 她就那樣靜靜地躺著潭千,像睡著了一般谱姓。 火紅的嫁衣襯著肌膚如雪借尿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天屉来,我揣著相機與錄音路翻,去河邊找鬼。 笑死茄靠,一個胖子當著我的面吹牛茂契,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播慨绳,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼掉冶,長吁一口氣:“原來是場噩夢啊……” “哼真竖!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起厌小,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤恢共,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后璧亚,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體讨韭,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年癣蟋,在試婚紗的時候發(fā)現(xiàn)自己被綠了透硝。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡疯搅,死狀恐怖濒生,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情幔欧,我是刑警寧澤甜攀,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站琐馆,受9級特大地震影響规阀,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瘦麸,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一谁撼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧滋饲,春花似錦厉碟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至呵曹,卻和暖如春款咖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背奄喂。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工铐殃, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人跨新。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓富腊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親域帐。 傳聞我的和親對象是個殘疾皇子赘被,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容