从MongoDB读取百万级数据并稳定导出至Excel的技术方案咨询
海量MongoDB聚合数据导出Excel的无崩溃方案
针对150万条数据的导出需求,核心是避免一次性加载所有数据到内存,用流式读取+流式写入的组合方案,完全替代当前把全量数据塞进ArrayList的操作,具体实现如下:
一、替换MongoDB聚合的全量读取方式
不要用into(new ArrayList<>())一次性加载所有聚合结果,改用MongoDB的游标流式读取,每次只拉取固定批次的数据(比如1000条),内存中始终只保留当前批次的数据集:
// 开启聚合游标,不一次性加载全量数据 AggregateIterable<Document> aggregateIterable = paReportingCollection.aggregate(pipeline); // 设置游标批次大小,控制每次从MongoDB拉取的数据量 aggregateIterable.batchSize(1000); // 获取迭代器,流式遍历数据 MongoCursor<Document> cursor = aggregateIterable.iterator();
二、选择流式Excel写入库
方案1:阿里EasyExcel(推荐,内存占用极低)
EasyExcel天生支持流式读写,无需手动处理内存,只需要自定义数据映射和迭代逻辑:
- 定义数据映射类(对应Excel列):
@Data public class ReportData { @ExcelProperty("字段1") private String field1; @ExcelProperty("字段2") private Integer field2; // 其他对应MongoDB Document的字段 }
- 流式写入逻辑:
// 输出流(Web场景直接用response的输出流即可) OutputStream outputStream = new FileOutputStream("report.xlsx"); // 初始化EasyExcel写入,绑定数据类并流式输出 EasyExcel.write(outputStream, ReportData.class) .sheet("报表数据") .doWrite(() -> new Iterator<ReportData>() { @Override public boolean hasNext() { return cursor.hasNext(); } @Override public ReportData next() { Document doc = cursor.next(); // 将MongoDB Document转换为映射类对象 ReportData data = new ReportData(); data.setField1(doc.getString("field1")); data.setField2(doc.getInteger("field2")); return data; } }); // 关闭游标释放资源 cursor.close();
方案2:Apache POI SXSSF(传统流式方案)
如果已经在用POI,可使用SXSSF设置内存窗口大小,超过窗口的行会自动写入磁盘,释放内存:
// 创建SXSSFWorkbook,设置内存保留1000行,超出部分写入临时文件 SXSSFWorkbook workbook = new SXSSFWorkbook(1000); SXSSFSheet sheet = workbook.createSheet("报表数据"); // 写入表头 Row headerRow = sheet.createRow(0); headerRow.createCell(0).setCellValue("字段1"); headerRow.createCell(1).setCellValue("字段2"); int rowNum = 1; try { while (cursor.hasNext()) { Document doc = cursor.next(); Row row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(doc.getString("field1")); row.createCell(1).setCellValue(doc.getInteger("field2")); // 每处理10000条手动刷新一次,避免临时文件过大 if (rowNum % 10000 == 0) { sheet.flushRows(); } } // 写入输出流 workbook.write(outputStream); } finally { // 清理临时文件、关闭资源 workbook.dispose(); cursor.close(); outputStream.close(); }
三、关键优化点
- 调整游标批次大小:根据内存情况把
batchSize设为500-2000,太小会增加数据库交互次数,太大仍会占用过多内存。 - Web场景异步处理:150万条数据导出耗时较长,建议改为异步生成文件,完成后通知用户下载(比如将文件存到内部存储,给用户发送下载链接)。
- 避免游标超时:如果导出时间超过MongoDB默认游标超时时间,可添加
aggregateIterable.noCursorTimeout(true),但必须手动关闭游标防止资源泄漏。 - 内存监控:导出过程中监控JVM内存,动态调整批次大小和Excel写入的窗口参数。
为什么不用分页?
分页方案需要多次执行聚合查询,用skip()和limit()拆分数据,但skip()会跳过大量数据,性能随页数增加急剧下降;且导出过程中数据若有新增/删除,会导致分页结果重复或遗漏。流式游标是一次聚合查询后持续拉取结果,性能稳定,数据一致性也更好。
内容的提问来源于stack exchange,提问作者Apurva Gupta
相关产品推荐
相关产品推荐

