大批量数据导出如何实现?

2025年 阅读约 10 分钟 面试指南 · Java面试

详解Java大批量数据导出方案,包括分批查询、流式写入、异步导出等技术选型与实现。

一句话总结

大批量数据导出需要分批查询 + 流式写入,避免一次性加载到内存导致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。