一句话总结
大批量数据导出需要分批查询 + 流式写入,避免一次性加载到内存导致OOM。推荐使用EasyExcel(基于SAX模式)或异步导出方案,确保高并发下的稳定性。
初级理解
核心问题:十多万数据一次性加载到内存会导致OOM,且同步请求超时。
分批查询:使用游标分页(WHERE id > lastId LIMIT 1000)而非OFFSET,避免深分页性能问题。
Long lastId = 0L;
List batch;
do {
batch = reportMapper.selectBatch(lastId, 1000);
if (!batch.isEmpty()) {
writer.write(batch, sheet);
lastId = batch.get(batch.size() - 1).getId();
}
} while (batch.size() == 1000);
一句话总结:分批查询 + 流式写入 = 大批量数据导出的核心方案。
中级深入
流式写入Excel:使用EasyExcel(基于SAX模式,内存占用极低)或POI SXSSFWorkbook(滑动窗口)。
@GetMapping("/export")
public void export(HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment;filename=report.xlsx");
try (ExcelWriter writer = EasyExcel.write(response.getOutputStream(), ReportVO.class).build()) {
WriteSheet sheet = EasyExcel.writerSheet("报表").build();
Long lastId = 0L;
List batch;
do {
batch = reportMapper.selectBatch(lastId, 1000);
if (!batch.isEmpty()) {
writer.write(batch, sheet);
lastId = batch.get(batch.size() - 1).getId();
}
} while (batch.size() == 1000);
}
}
注意:禁止使用XSSFWorkbook(全量内存模型),必须使用SXSSFWorkbook或EasyExcel。
高级拓展
异步导出(推荐用于超大数据量):
前端点击导出 → 后端返回任务ID → 后台异步生成文件上传至OSS → 前端轮询或WebSocket通知下载
适用于百万级以上数据或生成耗时 > 10秒的场景
防OOM关键点:
• 禁止SELECT *全量加载到List
• 禁止使用XSSFWorkbook(全量内存模型)
• 设置JVM参数-Xmx合理,配合-XX:+UseG1GC
• 导出完成后及时关闭流、释放资源
面试加分项:能说出异步导出方案、EasyExcel原理、防OOM策略,说明你对大数据量处理有实战经验。
实战场景
场景:电商报表导出(100万+数据)
@Service
public class ReportExportService {
@Async
public void exportAsync(Long userId, HttpServletResponse response) {
String taskId = UUID.randomUUID().toString();
redisTemplate.opsForHash().put("export:" + userId, taskId, "PROCESSING");
try (ExcelWriter writer = EasyExcel.write(response.getOutputStream(), ReportVO.class).build()) {
WriteSheet sheet = EasyExcel.writerSheet("报表").build();
Long lastId = 0L;
List batch;
do {
batch = reportMapper.selectBatch(lastId, 1000);
if (!batch.isEmpty()) {
writer.write(batch, sheet);
lastId = batch.get(batch.size() - 1).getId();
}
} while (batch.size() == 1000);
redisTemplate.opsForHash().put("export:" + userId, taskId, "COMPLETED");
} catch (Exception e) {
redisTemplate.opsForHash().put("export:" + userId, taskId, "FAILED");
}
}
}
面试模拟
Q:为什么不能用OFFSET分页?
A:OFFSET分页在深分页时性能极差。例如LIMIT 100000, 10,MySQL需要扫描100010条记录,然后丢弃前100000条。而游标分页(WHERE id > lastId LIMIT 10)只需要扫描10条记录。
Q:EasyExcel为什么比POI省内存?
A:EasyExcel基于SAX模式(事件驱动),逐行读取Excel文件,不需要将整个文件加载到内存。而POI的XSSFWorkbook是基于DOM模式,需要将整个Excel文件加载到内存,处理大文件时容易OOM。