本质是切断全量加载路径:数据库层用游标分页+fetchSize流式查询,写入层用EasyExcel流式写入OutputStream并禁用自动列宽等开销,资源层通过try-with-resources和finish()显式释放,架构层异步化并存至对象存储。

Java 大数据量导出(如百万级 Excel)时内存溢出,本质是数据、对象、资源在 JVM 堆中集中堆积所致。关键不是“加内存”,而是切断全量加载路径——用流式替代缓存,用分批替代一次拉取,用隔离替代共享。
数据库层:必须用游标分页,禁用 offset/limit
深分页(如 LIMIT 1000000, 1000)会触发全表扫描+内存排序,MyBatis 默认还会把整结果集缓存在 ResultSet 中。正确做法:
- 以主键或时间戳为游标,例如:WHERE create_time > ? ORDER BY create_time LIMIT 10000
- MyBatis 显式配置 fetchSize="1000",驱动启用流式 ResultSet(如 MySQL 的 useCursorFetch=true)
- 关闭二级缓存、懒加载代理,只查导出所需字段(DTO 投影),避免无用对象创建
写入层:走纯流式路径,不建 Workbook 实例
EasyExcel 默认已封装 SXSSF 流式能力,但需主动规避“伪流式”陷阱:
在调试 Java 后端 API 故障时使用此技能,尤其适用于以下任务:分析日志、跨 Controller、Service 和 Mapper 层追踪请求、定位异常根因、检查 SQL 执行或参数流转,以及生成简洁的诊断结论与修复方案。
- 调用 EasyExcel.write(OutputStream),而非 write(String fileName),跳过临时文件缓冲
- 每批 write 后自动 flush 行到 ZIP 条目,内存仅保留当前批次 List
对象 - 禁用自动列宽(registerWriteHandler(new SimpleWriteHandler()))、样式强引用、重复解析表头
资源与生命周期:显式释放,异常分支也不能漏
流、连接、临时 ZIP 条目不释放,会导致内存和磁盘双泄漏:
- ExcelWriter 必须 finish() —— 否则底层临时文件不会清理,磁盘持续增长
- 用 try-with-resources 包裹 OutputStream(如 BufferedOutputStream + FileOutputStream 或 HttpServletResponse.getOutputStream())
- 异步任务中,即使导出中途失败,也要确保 finish() 和 close() 在 finally 块中执行
架构层:脱离 Web 线程,导出结果存对象存储
百万行导出耗时数分钟,阻塞 Tomcat/Nginx 请求线程极危险:
- 前端提交后立即返回任务 ID,后端用线程池或消息队列(RabbitMQ/Kafka)触发导出
- 完成文件上传至 OSS/S3 或 FTP,返回可下载 URL,不走 HTTP 响应体直传
- 配合 Redis 存储任务状态,前端轮询或 WebSocket 推送进度,体验不卡顿
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










