面试官:百万数据导出Excel,如何避免OOM?
一、一个按钮引发的血案
上周二凌晨2点,我被报警电话吵醒:生产环境OOM,服务挂了。
罪魁祸首是一个平平无奇的”导出全部订单”按钮。运营同学想导出一批数据做分析,随手点了全量导出——100万条订单数据,瞬间打满堆内存,Full GC都救不回来,服务直接崩溃。
这题在面试中出场率极高,但很多人的回答停留在”分页查询”四个字。远远不够。今天从”最烂方案”到”最佳实践”,讲透5种方案的演进逻辑。
二、先算一笔账:OOM是怎么发生的
|
|
|
|---|---|
|
|
List<User> list = mapper.selectAll() |
|
|
XSSFWorkbook把数据全放内存 |
|
|
|
100万行数据 × 20列 × 平均字段长度,原始数据就500MB+。加上POI的Cell、Row、Sheet对象开销,内存轻松突破2GB。而你的服务堆内存可能只给了1G。
三、方案演进:从最差到最佳
方案一:naive写法(反面教材)
这是我最常看到的代码,也是OOM的罪魁祸首:
// 一次性全查全写List<User> all = userMapper.selectAll(); // 100万条进内存Workbook workbook = new XSSFWorkbook();Sheet sheet = workbook.createSheet();int rowNum = 0;for (User u : all) {Row row = sheet.createRow(rowNum++);row.createCell(0).setCellValue(u.getName());// ... 填充所有列}workbook.write(response.getOutputStream());
问题在哪?
-
100万条数据全进JVM堆
-
POI的XSSFWorkbook会把所有Row对象缓存到内存
-
查询和写入串行,内存双重占用
面试时如果这样答,基本凉凉。
方案二:分页查询 + 普通POI(初级改进)
意识到不能全查,于是改成批量:
Workbook workbook = new XSSFWorkbook();Sheet sheet = workbook.createSheet();int rowNum = 0;for (int page = 1; page <= 20; page++) {List<User> batch = userMapper.selectPage(page, 50000);for (User u : batch) {Row row = sheet.createRow(rowNum++);// 填充行...}}
看似改进,实则陷阱:
分页解决了数据加载问题,但XSSFWorkbook内部依然缓存所有Row对象。100万行的POI对象树,照样撑爆内存。
面试答到这一层,说明你有基础,但还不够。
方案三:CSV格式导出(曲线救国)
如果业务方只需要数据,不在乎字体颜色、合并单元格,CSV是最经济的选择:
response.setContentType("text/csv;charset=UTF-8");response.setHeader("Content-Disposition", "attachment; filename=data.csv");try (PrintWriter writer = response.getWriter()) {// 写入BOM头防Excel打开乱码writer.write('\ufeff');writer.println("ID,姓名,手机号,金额");// 流式查询,逐行写入jdbcTemplate.query("SELECT * FROM big_table", rs -> {writer.println(String.format("%d,%s,%s,%.2f",rs.getLong("id"),rs.getString("name"),rs.getString("phone"),rs.getDouble("amount")));});}
优势: 纯文本流式写入,内存占用接近0,100万行<10秒。
劣势: 无样式、无公式、无多Sheet,中文需处理BOM头。
面试时提到这个方案,说明你有产品思维,懂得权衡。
方案四:SXSSF流式导出(最佳实践)
这是生产环境的标准答案。SXSSF是POI提供的流式扩展,核心机制是滑动窗口:内存只保留指定行数(如100行),超出的数据刷入磁盘临时文件,像流水线一样持续运转。
// 滑动窗口100行,其余刷磁盘SXSSFWorkbook workbook = new SXSSFWorkbook(100);workbook.setCompressTempFiles(true); // 压缩临时文件SXSSFSheet sheet = workbook.createSheet("数据");// 创建表头Row header = sheet.createRow(0);header.createCell(0).setCellValue("ID");header.createCell(1).setCellValue("姓名");// ...// 分页查询,流式写入int rowNum = 1;for (int i = 0; i < total; i += 5000) {List<Data> batch = mapper.selectPage(i, 5000);for (Data d : batch) {Row row = sheet.createRow(rowNum++);row.createCell(0).setCellValue(d.getId());row.createCell(1).setCellValue(d.getName());// ...}batch.clear(); // 助攻GCsheet.flushRows(5000); // 强制刷盘,释放内存}workbook.write(response.getOutputStream());workbook.dispose(); // 必须清理临时文件!
关键参数:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
避坑点:
-
临时文件目录磁盘要充足(默认
java.io.tmpdir) -
CellStyle要复用,别每行新建(样式过多也会OOM)
-
大数据量禁用自动计算公式
-
必须用
dispose()清理临时文件,或用try-with-resources
实测数据:100万行导出,XSSF模式需4GB内存,SXSSF仅需50MB,耗时从180秒降到90秒。
面试答到这一层,已经能拿高分。
方案五:异步导出 + OSS(高并发终极大招)
当面试官追问:”如果10个用户同时导出百万数据怎么办?”——这就是答案。
架构流程:
用户点击导出 → 提交任务 → 返回任务ID → 后台分批生成 → 上传OSS → 通知用户下载
像点外卖,不等做好再响应,而是给订单号,好了通知你。
核心优势:
-
不占用应用服务器内存
-
支持断点续传、重复下载
-
可限制导出频率防刷
-
超大数据(500万+)也能处理
面试提到异步架构,说明你有系统思维,能应对高并发场景。
四、方案对比总结
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
五、面试加分项
进阶回答:
“除了技术方案,还要考虑产品层面——超过10万条数据,是否应该引导用户用筛选条件而非全量导出?技术上防OOM,产品上防滥用。”
一句话总结:
“大数据导出的本质,是用时间换空间——把’先存再写’变成’边查边写’。”
六、结语
OOM问题,表面是技术问题,实际是思维问题——永远假设用户会点”导出全部”。
你们公司导出功能上限设了多少条?有没有遇到过OOM惨案?评论区聊聊。
夜雨聆风