批量导出几十万行就OOM:SXSSF流式写入与分页查询的落地实践
上周三下午快下班,运营跑过来甩了张截图,说批量导出物流查询结果直接把服务搞挂了,日志里一行java.lang.OutOfMemoryError: Java heap space,导出按钮点一次崩一次。数据量也就40万行,测试环境用几百行测都没事。我们花了两天把导出模块从内存全量加载改成流式写入加分页查询,下面把过程记一下。
原来的代码逻辑很典型:先SELECT *把40万行全查出来塞List,再new XSSFWorkbook()逐行写,最后write到response。XSSFWorkbook整个workbook都在内存里,40万行加上样式对象,堆直接拉满。
原始流程:
1. JDBC ResultSet全量加载到List<Map> -> 40万行对象
2. XSSFWorkbook在内存构建sheet -> 每一行一个Row对象
3. outputStream.write(workbook.getBytes()) -> 一次性写出第一步和第二步都在堆里,两个加起来就是OOM的根源。XSSF是DOM模式,整个文档树常驻内存;要解决就得换成SAX式的SXSSF,它只在内存保留一个滑动窗口的行。
Apache POI的SXSSFWorkbook可以设置窗口大小,超出窗口的行刷到磁盘临时文件。我们用的参数:
SXSSFWorkbook wb = new SXSSFWorkbook(500);
Sheet sheet = wb.createSheet("result");
// 每写500行,之前的行flush到磁盘临时文件窗口大小500是个平衡点:太小会频繁磁盘IO,太大内存降不下来。我们测下来500行窗口,40万行导出时堆占用从原来的1.8GB降到了不到300MB。
这里踩过一个坑:SXSSF默认在系统临时目录生成xml临时文件,跑完不手动dispose会残留磁盘垃圾。必须在finally里调wb.dispose()清临时文件。
光换写入还不够,SELECT *全量查40万行照样把JDBC结果集撑爆。改成分页查询,关键是每页按主键游标推进,不能用OFFSET。
错误做法:LIMIT offset, size
- offset=399000时MySQL要扫前399000行再丢弃,越翻越慢
正确做法:WHERE id > lastId ORDER BY id ASC LIMIT size
- 主键有序,每次从上一次最大id开始,走索引恒定快每页大小我们设成2000行,查完一页写一页,写完全部再拉下一页。这样任何时刻内存里只有当前页2000行,堆占用稳定。
环节 | 改造前 | 改造后 |
|---|---|---|
查询方式 | SELECT *全量到List | id游标分页,每页2000 |
写入方式 | XSSFWorkbook全内存 | SXSSF窗口500行 |
堆峰值 | 1.8GB | 约280MB |
40万行耗时 | OOM直接崩 | 约45秒完成 |
临时文件 | 无 | 跑完dispose清理 |
这次导出需求是配合固乔快递批量查询助手的批量查询结果做的,我主要负责把导出模块从内存全量加载改成流式分页,上线后40万行导出稳定跑通没再崩过。
以上是个人实践记录,各平台具体功能以官方实时信息为准。
留个问题:你们导出百万行级别数据时,是走POI流式还是直接CSV流?CSV虽然没样式但性能确实差一个量级。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。