乐于分享
好东西不私藏

面试官:百万数据导出Excel,如何避免OOM?

面试官:百万数据导出Excel,如何避免OOM?

一、一个按钮引发的血案

上周二凌晨2点,我被报警电话吵醒:生产环境OOM,服务挂了。

罪魁祸首是一个平平无奇的”导出全部订单”按钮。运营同学想导出一批数据做分析,随手点了全量导出——100万条订单数据,瞬间打满堆内存,Full GC都救不回来,服务直接崩溃。

这题在面试中出场率极高,但很多人的回答停留在”分页查询”四个字。远远不够。今天从”最烂方案”到”最佳实践”,讲透5种方案的演进逻辑。


二、先算一笔账:OOM是怎么发生的

罪魁祸首
具体表现
全量加载
List<User> list = mapper.selectAll()
POI内存模式
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());

问题在哪?

  1. 100万条数据全进JVM堆

  2. POI的XSSFWorkbook会把所有Row对象缓存到内存

  3. 查询和写入串行,内存双重占用

面试时如果这样答,基本凉凉。


方案二:分页查询 + 普通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(); // 助攻GC    sheet.flushRows(5000); // 强制刷盘,释放内存}workbook.write(response.getOutputStream());workbook.dispose(); // 必须清理临时文件!

关键参数:

参数
建议值
说明
滑动窗口
100-1000行
太小IO频繁,太大内存涨
fetchSize
1000-5000
JDBC流式读取批次
压缩临时文件
true
减少磁盘占用

避坑点:

  1. 临时文件目录磁盘要充足(默认java.io.tmpdir

  2. CellStyle要复用,别每行新建(样式过多也会OOM)

  3. 大数据量禁用自动计算公式

  4. 必须用dispose()清理临时文件,或用try-with-resources

实测数据:100万行导出,XSSF模式需4GB内存,SXSSF仅需50MB,耗时从180秒降到90秒。

面试答到这一层,已经能拿高分。


方案五:异步导出 + OSS(高并发终极大招)

当面试官追问:”如果10个用户同时导出百万数据怎么办?”——这就是答案。

架构流程:

用户点击导出 → 提交任务 → 返回任务ID → 后台分批生成 → 上传OSS → 通知用户下载

像点外卖,不等做好再响应,而是给订单号,好了通知你。

核心优势:

  • 不占用应用服务器内存

  • 支持断点续传、重复下载

  • 可限制导出频率防刷

  • 超大数据(500万+)也能处理

面试提到异步架构,说明你有系统思维,能应对高并发场景。


四、方案对比总结

方案
内存占用
适用数据量
用户体验
推荐指数
naive全量加载
爆炸
<1万
同步等待
分页+普通POI
<10万
同步等待
⭐⭐
CSV导出
极低
无上限
同步等待
⭐⭐⭐⭐
SXSSF流式
极低
百万级
同步等待
⭐⭐⭐⭐⭐
异步+OSS
不占用
无上限
异步通知
⭐⭐⭐⭐⭐

五、面试加分项

进阶回答:

“除了技术方案,还要考虑产品层面——超过10万条数据,是否应该引导用户用筛选条件而非全量导出?技术上防OOM,产品上防滥用。”

一句话总结:

“大数据导出的本质,是用时间换空间——把’先存再写’变成’边查边写’。”


六、结语

OOM问题,表面是技术问题,实际是思维问题——永远假设用户会点”导出全部”。

你们公司导出功能上限设了多少条?有没有遇到过OOM惨案?评论区聊聊。