Java實(shí)現(xiàn)百萬(wàn)數(shù)據(jù)導(dǎo)出Excel的詳細(xì)指南
本文分享一個(gè)Java開發(fā)者從初出茅廬到技術(shù)老手的成長(zhǎng)歷程,聚焦百萬(wàn)級(jí)數(shù)據(jù)導(dǎo)出場(chǎng)景,看如何從OOM崩潰走向優(yōu)雅解決。
一、新手踩坑:我的第一個(gè)導(dǎo)出功能
剛?cè)胄心悄?,我接到第一個(gè)獨(dú)立任務(wù):實(shí)現(xiàn)訂單數(shù)據(jù)導(dǎo)出Excel。當(dāng)時(shí)憑著學(xué)校學(xué)的基礎(chǔ)知識(shí),寫出了這樣的代碼:
// 新手版導(dǎo)出代碼 - 內(nèi)存炸彈!
public void exportOrders(HttpServletResponse response) {
// 1. 一次性加載全量數(shù)據(jù)
List<Order> allOrders = orderDao.findAll();
// 2. 創(chuàng)建Excel對(duì)象(當(dāng)時(shí)還不知道內(nèi)存代價(jià))
Workbook workbook = new HSSFWorkbook();
Sheet sheet = workbook.createSheet("Orders");
// 3. 逐行填充數(shù)據(jù)
int rowNum = 0;
for (Order order : allOrders) { // 百萬(wàn)次循環(huán)
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(order.getId());
row.createCell(1).setCellValue(order.getAmount());
// ...15+個(gè)字段
}
// 4. 寫入響應(yīng)流
workbook.write(response.getOutputStream());
}
第一次壓測(cè)時(shí)的災(zāi)難現(xiàn)場(chǎng):
Exception in thread "http-nio-8080-exec-3" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
// 堆棧指向Excel對(duì)象創(chuàng)建
二、錯(cuò)誤分析:為什么新手代碼會(huì)OOM
三重內(nèi)存炸彈:
- 數(shù)據(jù)對(duì)象駐留內(nèi)存:百萬(wàn)條Order對(duì)象(約1.2GB)
- Excel DOM樹爆炸:POI的HSSFWorkbook每個(gè)單元格都是獨(dú)立對(duì)象
- 字符串拼接黑洞:字段值拼接消耗額外內(nèi)存
內(nèi)存消耗估算:
| 組件 | 1萬(wàn)條 | 10萬(wàn)條 | 100萬(wàn)條 |
|---|---|---|---|
| 訂單對(duì)象 | 120MB | 1.2GB | 12GB |
| Excel對(duì)象(估算) | 200MB | 2GB | 20GB+ |
| 總內(nèi)存 | 320MB | 3.2GB | 32GB+ |
當(dāng)時(shí)我用的測(cè)試機(jī)只有2G內(nèi)存...
三、解決之道:流式處理方案
架構(gòu)演進(jìn)對(duì)比
graph LR
A[新手方案] -->|全內(nèi)存| B[OOM崩潰]
C[優(yōu)化方案] -->|磁盤緩沖| D[成功導(dǎo)出]
D -->|內(nèi)存控制| E[穩(wěn)定運(yùn)行]
核心代碼改造(基于POI SXSSF)
public void streamExport(HttpServletResponse response) throws Exception {
// 1. 創(chuàng)建流式工作簿(內(nèi)存中只保留100行)
Workbook workbook = new SXSSFWorkbook(100);
Sheet sheet = workbook.createSheet("訂單數(shù)據(jù)");
// 2. 寫表頭
Row header = sheet.createRow(0);
header.createCell(0).setCellValue("ID");
// ...其他表頭
// 3. 分頁(yè)查詢+流式寫入
int pageSize = 2000;
int pageNum = 1;
int rowIndex = 1; // 數(shù)據(jù)行起始位置
while (true) {
// 4. 分頁(yè)查詢(避免全量加載)
List<Order> page = orderDao.findByPage(pageNum, pageSize);
if (page.isEmpty()) break;
// 5. 批量寫入當(dāng)前頁(yè)
for (Order order : page) {
Row row = sheet.createRow(rowIndex++);
row.createCell(0).setCellValue(order.getId());
// ...其他字段
}
// 6. 刷新當(dāng)前頁(yè)數(shù)據(jù)到磁盤
((SXSSFSheet)sheet).flushRows(page.size());
pageNum++;
}
// 7. 設(shè)置響應(yīng)頭
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment;filename=orders.xlsx");
// 8. 流式輸出到客戶端
workbook.write(response.getOutputStream());
// 9. 清理臨時(shí)文件
((SXSSFWorkbook)workbook).dispose();
}
四、關(guān)鍵技術(shù)解析
1.SXSSFWorkbook 核心機(jī)制
滑動(dòng)窗口:內(nèi)存中只保留指定行數(shù)(默認(rèn)100行)
自動(dòng)刷盤:超過(guò)窗口大小的行寫入磁盤臨時(shí)文件
內(nèi)存對(duì)比:傳統(tǒng)方式 vs SXSSF
// 傳統(tǒng)方式(危險(xiǎn)?。? Workbook workbook = new XSSFWorkbook(); // 流式處理(安全) Workbook workbook = new SXSSFWorkbook(100);
2.分頁(yè)查詢優(yōu)化技巧
避免深度分頁(yè):不要使用limit 1000000,100
推薦方案:基于ID范圍的連續(xù)分頁(yè)
SELECT * FROM orders WHERE id > ? ORDER BY id LIMIT 2000
3.內(nèi)存監(jiān)控技巧
添加JVM參數(shù)觀察內(nèi)存變化:
-XX:+PrintGCDetails -Xloggc:gc.log
五、性能優(yōu)化實(shí)戰(zhàn)
樣式處理陷阱
// 錯(cuò)誤做法:每行創(chuàng)建樣式(內(nèi)存爆炸)
for(Order order : orders) {
CellStyle style = workbook.createCellStyle();
row.setCellStyle(style);
}
// 正確做法:樣式池復(fù)用
CellStyle moneyStyle = workbook.createCellStyle();
moneyStyle.setDataFormat(BuiltinFormats.getBuiltinFormat(4));
// 在需要時(shí)直接使用
cell.setCellStyle(moneyStyle);
臨時(shí)文件管理
// 自定義臨時(shí)文件位置(避免/tmp爆滿)
File tmpDir = new File("/data/tmp");
SXSSFWorkbook workbook = new SXSSFWorkbook(null, 100, true, tmpDir);
寫入加速技巧
// 批量設(shè)置單元格值(減少方法調(diào)用)
Row row = sheet.createRow(0);
Object[] values = {"ID", "金額", "日期"};
for (int i = 0; i < values.length; i++) {
row.createCell(i).setCellValue(values[i].toString());
}
六、方案效果對(duì)比
| 指標(biāo) | 新手方案 | 流式方案 |
|---|---|---|
| 內(nèi)存占用 | >3GB (OOM) | ≈150MB |
| 響應(yīng)時(shí)間 | 無(wú)法完成 | 5分鐘/百萬(wàn)行 |
| CPU占用 | 頻繁Full GC | 平穩(wěn) |
| 代碼復(fù)雜度 | 簡(jiǎn)單 | 中等 |
| 可支持?jǐn)?shù)據(jù)量 | <1萬(wàn)行 | >1000萬(wàn)行 |
七、避坑指南:血淚經(jīng)驗(yàn)
分頁(yè)查詢的坑
// MySQL深度分頁(yè)性能陷阱
List<Order> list = orderDao.query("SELECT * FROM orders LIMIT 900000,1000");
資源關(guān)閉的坑
// 忘記關(guān)閉資源導(dǎo)致內(nèi)存泄漏 Workbook workbook = new SXSSFWorkbook(); // 必須添加finally塊關(guān)閉
數(shù)據(jù)類型的坑
// 日期類型特殊處理
CellStyle dateStyle = workbook.createCellStyle();
dateStyle.setDataFormat(workbook.createDataFormat().getFormat("yyyy-MM-dd"));
cell.setCellStyle(dateStyle);
八、老鳥的思考
8年Java開發(fā)教會(huì)我處理海量數(shù)據(jù)的核心原則:
內(nèi)存有限性原則:
graph LR 內(nèi)存-->磁盤-->分布式
當(dāng)內(nèi)存不夠時(shí),合理利用磁盤空間
流式處理三要素:
- 分頁(yè)加載(Paging)
- 批量處理(Batching)
- 即時(shí)釋放(Releasing)
資源管理箴言:
"打開的資源要及時(shí)關(guān)閉,創(chuàng)建的對(duì)象要明確生命周期"
最后建議:超過(guò)500萬(wàn)行數(shù)據(jù)建議使用CSV格式或?qū)I(yè)ETL工具,Excel畢竟不是數(shù)據(jù)庫(kù)!
以上就是Java實(shí)現(xiàn)百萬(wàn)數(shù)據(jù)導(dǎo)出Excel的詳細(xì)指南的詳細(xì)內(nèi)容,更多關(guān)于Java百萬(wàn)數(shù)據(jù)導(dǎo)出Excel的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- 使用Java實(shí)現(xiàn)百萬(wàn)Excel數(shù)據(jù)導(dǎo)出
- Java表數(shù)據(jù)導(dǎo)出到Excel中的實(shí)現(xiàn)
- 使用java實(shí)現(xiàn)百萬(wàn)級(jí)別數(shù)據(jù)導(dǎo)出excel的三種方式
- 詳解Java如何實(shí)現(xiàn)百萬(wàn)數(shù)據(jù)excel導(dǎo)出功能
- Java樹形結(jié)構(gòu)數(shù)據(jù)生成導(dǎo)出excel文件方法記錄
- Java大批量導(dǎo)出Excel數(shù)據(jù)的優(yōu)化過(guò)程
- Java中用POI實(shí)現(xiàn)將數(shù)據(jù)導(dǎo)出到Excel
- Java實(shí)現(xiàn)Excel導(dǎo)入導(dǎo)出數(shù)據(jù)庫(kù)的方法示例
- java導(dǎo)出大批量(百萬(wàn)以上)數(shù)據(jù)的excel文件
相關(guān)文章
詳解SpringBoot中異步請(qǐng)求和異步調(diào)用(看完這一篇就夠了)
這篇文章主要介紹了SpringBoot中異步請(qǐng)求和異步調(diào)用問(wèn)題,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-04-04
關(guān)于java String中intern的深入講解
這篇文章主要給大家介紹了關(guān)于java String中intern的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
Java實(shí)現(xiàn)求子數(shù)組和的最大值算法示例
這篇文章主要介紹了Java實(shí)現(xiàn)求子數(shù)組和的最大值算法,涉及Java數(shù)組遍歷、判斷、運(yùn)算等相關(guān)操作技巧,需要的朋友可以參考下2018-02-02
詳談java中File類getPath()、getAbsolutePath()、getCanonical的區(qū)別
下面小編就為大家?guī)?lái)一篇詳談java中File類getPath()、getAbsolutePath()、getCanonical的區(qū)別。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-07-07
springboot使用自定義注解實(shí)現(xiàn)aop切面日志
這篇文章主要為大家詳細(xì)介紹了springboot使用自定義注解實(shí)現(xiàn)aop切面日志,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-09-09
java實(shí)現(xiàn)十六進(jìn)制字符unicode與中英文轉(zhuǎn)換示例
當(dāng)需要對(duì)一個(gè)unicode十六進(jìn)制字符串進(jìn)行編碼時(shí),首先做的應(yīng)該是確認(rèn)字符集編碼格式,在無(wú)法快速獲知的情況下,通過(guò)一下的str4all方法可以達(dá)到這一目的2014-02-02

