实战避坑用MyBatis CursorSpring Boot优雅导出50万Excel内存只占20MB后台管理系统中最令人头疼的需求之一莫过于导出全部数据这个看似简单的功能按钮。当产品经理轻描淡写地说这个报表需要支持导出全部50万条记录时作为开发者的你是否感到后背发凉传统分页查询导出耗时过长一次性加载又可能导致服务内存溢出。本文将揭秘如何通过MyBatis Cursor与Spring Boot的黄金组合实现内存占用仅20MB的百万级数据Excel导出方案。1. 为什么传统导出方案会爆内存在电商订单导出或用户数据备份场景中开发者常采用两种典型方案// 方案一全量加载到内存 ListUser allUsers userMapper.selectAll(); writeToExcel(allUsers); // 瞬间内存暴涨 // 方案二分页批次处理 for(int i0; itotalPage; i){ PageUser page userMapper.selectPage(new Page(i, 5000)); writeToExcel(page.getRecords()); // 多次IO操作耗时剧增 }这两种方式的内存表现对比如下方案50万条数据内存占用耗时数据库压力全量加载800MB~1.2GB中等一次性高传统分页50MB~100MB很长持续波动Cursor流式20MB以下较短平稳内存杀手的根源在于JDBC默认行为执行查询时会先将所有结果集缓存在内存中。即使使用MyBatis的分页插件也只是在应用层分页无法解决数据库驱动层面的内存占用问题。2. MyBatis Cursor的流式救赎方案2.1 核心配置揭秘让MyBatis真正实现流式查询的关键配置public interface UserMapper { Select(SELECT * FROM large_user_table) Options(fetchSize Integer.MIN_VALUE) CursorUser selectAllWithCursor(); }这个fetchSize Integer.MIN_VALUE的魔法参数会触发MySQL JDBC驱动的流式结果集模式。其工作原理是数据库保持连接打开状态逐条传输结果而非批量加载应用端通过迭代器逐条消费重要提示必须保持事务开启状态直到Cursor处理完毕否则连接关闭会导致数据获取中断2.2 Spring Boot中的完整实现结合Spring事务管理的典型服务层代码Service RequiredArgsConstructor public class ExportService { private final UserMapper userMapper; Transactional(readOnly true) public void exportLargeData(HttpServletResponse response) throws IOException { try (CursorUser cursor userMapper.selectAllWithCursor(); SXSSFWorkbook workbook new SXSSFWorkbook(100)) { Sheet sheet workbook.createSheet(Users); int rowNum 0; for (User user : cursor) { Row row sheet.createRow(rowNum); row.createCell(0).setCellValue(user.getId()); row.createCell(1).setCellValue(user.getName()); // 其他字段处理... if(rowNum % 100 0) { sheet.flushRows(); // 定期刷新行到磁盘 } } response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); workbook.write(response.getOutputStream()); } } }这里组合了三个关键技术MyBatis Cursor流式获取数据Apache POI的SXSSFWorkbook实现Excel流式写入Spring事务确保数据库连接持续可用3. 性能优化与避坑指南3.1 连接池关键配置使用流式查询时连接池需要特殊调整# application.yml spring: datasource: hikari: maximum-pool-size: 20 connection-timeout: 60000 max-lifetime: 1800000需要特别注意增大超时时间大数据量导出可能耗时较长避免连接泄漏确保在finally块中关闭Cursor隔离级别建议使用READ_COMMITTED避免长事务问题3.2 内存监控对比使用JVisualVM监控两种方案的内存曲线![内存占用对比图]传统方式内存呈阶梯式上升最终触发GCCursor方案内存稳定在20MB左右波动3.3 常见问题排查清单数据中断检查是否在事务外使用Cursor确认数据库连接没有超时断开导出速度慢调整SXSSFWorkbook的windowSize参数(默认100)检查数据库索引是否合理内存仍然过高确保没有在循环中累积数据集合验证fetchSize确实设置为Integer.MIN_VALUE4. 进阶场景与替代方案4.1 超大规模数据导出当数据量超过千万级时可以考虑分片并行导出// 按ID范围分片处理 ExecutorService executor Executors.newFixedThreadPool(4); ListFutureFile futures new ArrayList(); for(int i0; i4; i){ final int segment i; futures.add(executor.submit(() - exportSegment(segment * 250000, (segment1) * 250000) )); } // 合并结果文件...直接数据库导出-- MySQL直接导出到文件 SELECT * INTO OUTFILE /tmp/export.csv FIELDS TERMINATED BY , FROM large_table;4.2 其他数据库适配不同数据库的流式配置差异数据库流式查询关键配置注意事项MySQLfetchSizeInteger.MIN_VALUE需要保持事务OraclefetchSize100~500建议使用READ_ONLY结果集PostgreSQLfetchSize1000自动支持流式SQL ServeruseCursorFetchtrue需要特殊连接参数5. 生产环境实战建议在实际项目中落地该方案时建议采用以下策略渐进式实施先在非核心功能试点对比日志分析性能提升效果逐步替换原有导出接口监控指标记录每次导出的内存峰值监控数据库连接占用时长统计导出失败率用户体验优化前端添加进度提示支持断点续传提供异步导出邮件通知最近在处理一个跨境电商平台的订单导出需求时这套方案成功将原本频繁OOM的导出功能优化为稳定支持百万级数据导出。关键发现是必须同时控制好POI的rowAccessWindowSize和MyBatis的fetchSize两者协同才能达到最佳内存平衡点。