必须拆分查询、生成、下载三环节,oracle用主键或时间戳游标分页(禁offset),easyexcel流式写入+分sheet,导出任务异步化并状态轮询,注意时区与null游标处理。
直接用 easyexcel.write() 一次性查完再写,50 万行就容易 oom 或超时。必须拆开数据查询、文件生成、下载三个环节,重点压住 oracle 查询和内存两头。
Oracle 分页必须用游标,不能用 OFFSET
深分页(比如 OFFSET 1000000 LIMIT 10000)会让 Oracle 回表扫描大量无用数据,越往后越慢。实际压测中,第 100 页耗时可能是第 1 页的 8 倍以上。
- 用主键或时间戳字段做游标,例如
WHERE create_time > ? ORDER BY create_time, id,每次传上一页最后一条的create_time和id - 确保该字段有联合索引,否则游标失效
- 避免在 WHERE 条件里用函数(如
TO_CHAR(create_time)),会跳过索引 - MyBatis-Plus 的
page默认走OFFSET,得手动拼游标 SQL 或用CursorPage插件
EasyExcel 必须开启流式写入 + 分 Sheet
单 Sheet 超过 1048576 行会报错,但更早就会因内存撑不住崩溃。流式写入不是“自动省内存”,而是靠分批 flush 控制驻留对象数。
- 用
EasyExcel.write(outputStream, clazz).inMemory(false)强制关闭内存缓存 - 每写满 50 万行就调用
writeSheet()新建一个 Sheet,同时显式clear()当前 sheet 数据 - 不要用
doWrite(list)全量传参,改用write(dataList, writeSheet)分批提交 - 如果字段含富文本或图片,禁用样式自动计算:
.autoTrim(false).registerConverter(...)
导出任务必须异步化,且带状态轮询
HTTP 请求扛不住 3 分钟以上的连接,Nginx 默认 60 秒超时,前端刷新后任务就丢了。异步不是加个 @Async 就完事。
- 用户请求只返回任务 ID,例如
{"taskId": "export_20260623_abc123"} - 后台用线程池或消息队列执行,结果存到 Redis(带 TTL)或 DB 的
export_task表 - 前端轮询
/export/status?taskId=xxx,返回{"status": "success", "url": "/download/xxx.xlsx"}或失败原因 - 失败时记录具体阶段:是
query_failed、write_failed还是upload_failed,方便定位
临时文件路径和清理逻辑不能省
流式写入最终还是要落盘——哪怕只是临时 ZIP 包,不清理就会把磁盘打满。Linux 上 /tmp 可能被定时清理,不能直接写那里。
- 固定目录如
/data/export/tmp/,确保应用有读写权限 - 文件名必须带时间戳和随机后缀,避免并发覆盖:
export_20260623_abc123_8a9f.xlsx - 成功下载后触发异步删除;失败则 24 小时后由定时任务兜底清理
- 若用 MinIO/S3 存储,上传完成后立即删本地临时文件,别等响应返回
最容易被忽略的是 Oracle 游标字段的时区和空值处理——create_time 是 TIMESTAMP WITH TIME ZONE 类型时,Java 侧没对齐时区会导致漏数据;NULL 值游标比较会中断分页链。这两点线上故障率远高于内存配置错误。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











