
说好不内存溢出的 EasyExcel为什么一上线还是把服务干 OOM 了很多人觉得既然用了号称防止 OOM 的 EasyExcel大数据量导出就稳了。结果写完接口一压测或者上了生产环境还是眼睁睁看着堆内存报警服务直接挂掉。大家肯定纳闷这 EasyExcel 难道是虚假宣传其实跟 EasyExcel 没关系这锅得业务代码来背。今天咱们盘一盘用 EasyExcel 导出一两百万条数据时到底哪里吃掉了内存。内存到底去哪了以前大家用原生 Apache POI 导出POI 会把所有数据、样式、单元格对象全缓存在内存里等到最后再一把写进文件。一百万行数据POI 可能要吃掉几个 G 的内存。EasyExcel 确实解决了这个问题它的核心做法是一行行处理边解析边刷盘。所以在它这块内存是绝对控得住的。问题出在传给 EasyExcel 的数据源上。很多人的导出接口是这么写的// 1. 一次性从数据库查出 100 万条数据ListUserDTOallDatauserMapper.selectAllUsers();// 2. 一次性扔给 EasyExcel 去写EasyExcel.write(user.xlsx,UserDTO.class).sheet(模板).doWrite(allData);看出来了吧EasyExcel 是没占多少内存但 MyBatis 查出来的这 100 万个UserDTO对象全在堆内存里堆着。如果你一个对象占 1KB100 万个对象就是将近 1GB 内存。这时候再来两个运营同时点一下导出老年代当场爆满直接触发 Full GC接着就是 OOM 退出。怎么做到真正的低内存消耗想在低内存下导完大数据整个数据流都必须是“流式”的。不能只靠写文件流式查数据库也得一段一段来。这里有两种常用的做法。第一种分页查分批写这是最直接的做法。一次只查 5000 条丢给 EasyExcel 写然后再查下个 5000 条。内存里永远只有 5000 个对象。try(ExcelWriterexcelWriterEasyExcel.write(user.xlsx,UserDTO.class).build()){WriteSheetwriteSheetEasyExcel.writerSheet(用户数据).build();intpageSize5000;for(inti1;itotalPage;i){// 每次只查一页的数据ListUserDTOdatauserMapper.selectPage(i,pageSize);excelWriter.write(data,writeSheet);}}好处是代码好写也不怎么挑数据库。代价就是分页查询的通病越往深处翻页越慢。比如 MySQL 里的LIMIT 1000000, 5000数据库要扫描一百多万行然后丢弃性能很差。如果要优化深分页还得配合主键游标来查比如根据上次查询的最大 ID 来检索。第二种MyBatis 游标查询CursorMyBatis 支持返回CursorT。这玩意相当于数据库的游标你调用iterator.next()才会去数据库取数据。// Mapper 接口Select(SELECT * FROM user)CursorUserDTOstreamQuery();// 业务代码try(CursorUserDTOcursoruserMapper.streamQuery();ExcelWriterexcelWriterEasyExcel.write(user.xlsx,UserDTO.class).build()){WriteSheetwriteSheetEasyExcel.writerSheet(用户数据).build();ListUserDTObuffernewArrayList();for(UserDTOuser:cursor){buffer.add(user);// 攒够 5000 条写一次if(buffer.size()5000){excelWriter.write(buffer,writeSheet);buffer.clear();}}// 写最后剩下的一点数据if(!buffer.isEmpty()){excelWriter.write(buffer,writeSheet);}}这个方案把数据库到底层的交互也变成了流式完全解决了深分页慢的问题。但要注意两个坑必须在事务内执行MyBatis 游标必须保持数据库连接不关闭通常得在方法上加Transactional保证整个循环期间连接存活。长时间占着连接如果你写文件很慢数据库连接就会一直被你占着。并发稍微大一点数据库连接池很容易被耗尽影响其他正常业务。怎么做选择绝大多数后台管理系统直接用分页查分批写就行如果太慢就做主键 ID 偏移量分页。这招不用一直占着数据库连接最稳妥。要是导出任务实在太重或者面向 C 端用户那就别做成同步等待的接口。让用户点完按钮后去干别的后台开个异步任务慢慢跑生成完了给用户发个系统通知或者邮件提供 OSS 下载链接。