核心解法是分页并发查询+边查边写:先查总数分页,用线程池并发查每页数据,查完立即写入excel流,避免全量加载oom;需隔离事务、透传权限标识,并通过redis限流和超时控制保障稳定性。

直接用单线程查完再导出,20万条数据轻松卡15秒以上,还容易OOM。核心解法就一条:把“查数据”这个耗时动作拆开,并行跑,查完立刻写入Excel流——不是等全部查完才动笔。
分页+线程池是提速关键
一次查20万条,数据库压力大、JVM堆内存扛不住;但每次只查1万条,查20次,就可控得多。关键是这20次不用串行等,而是扔进线程池并发执行:
- 先查总数,比如213,456条 → 算出要分22页(每页1万)
- 建一个固定大小线程池,比如8个核心线程(根据CPU核数设)
- 循环提交22个任务,每个任务负责查第i页(offset = i × 10000, limit = 10000)
- 用Future收集结果,主线程调
get()阻塞等待全部完成
避免事务和数据权限踩坑
异步线程里查不到刚提交的数据?很可能是事务没提交完,线程就去查了。解决方法有两个:
- 确保查询逻辑不在同一个事务上下文里执行(比如把导出方法标记
@Transactional(propagation = Propagation.NOT_SUPPORTED)) - 如果用的是远程服务调用(如Feign),务必把用户身份、租户ID、数据权限标识透传过去,否则线程查出来的可能是空或越权数据
导出阶段别再全量加载到内存
查完22批List,别一股脑放进一个大List再交给EasyExcel——那又回到OOM老路。正确做法是边查边写:
- 用
EasyExcel.write(outputStream, Xxx.class).autoCloseStream(false)拿到ExcelWriter - 每个线程查完一批,立刻调
excelWriter.write(list, writeSheet) - 所有线程结束后,再
excelWriter.finish()
这样内存里始终只存当前页数据+Excel缓存窗口(默认1000行),百万级也能稳住。
加一层任务管控更稳妥
用户狂点导出按钮怎么办?得防住资源被打爆:
- 用
CountDownLatch或CompletableFuture.allOf()控制并发批次完成信号 - 全局加Redis计数器,限制同一时刻最多5个导出任务运行
- 超时自动中断:给
Future.get(30, TimeUnit.SECONDS)加超时,失败则清理临时资源并报错










