百万级数据导出需“边查边写”,核心是JDBC流式读取(TYPE_FORWARD_ONLY+CONCUR_READ_ONLY+setFetchSize(Integer.MIN_VALUE))、MyBatis使用Cursor惰性遍历、Excel用SXSSF或EasyExcel流式API逐行写入,并手动管理连接生命周期确保三者对齐。

直接用流式查询导出百万级数据,核心是“边查边写”,不把整张表加载进内存。关键不在 MyBatis 封装层,而在 JDBC 底层行为控制——必须让 MySQL 驱动以流式方式逐行返回结果,再配合流式写入工具(如 SXSSF 或 EasyExcel 的流式 API),才能真正压住内存峰值。
MySQL 驱动必须开启流式读取
默认情况下,MySQL JDBC 驱动会缓存整个结果集到客户端内存。要启用服务端游标、逐行拉取,需同时满足两个条件:
- 创建 Statement 或 PreparedStatement 时,指定 TYPE_FORWARD_ONLY 和 CONCUR_READ_ONLY
- 调用 setFetchSize(Integer.MIN_VALUE)(不是设成大数字,而是最小值)
示例代码片段:
Connection conn = dataSource.getConnection();
// 关键:声明游标类型 + 设置流式 fetch size
PreparedStatement ps = conn.prepareStatement(
"SELECT id, name, email FROM user WHERE status = ?",
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY
);
ps.setFetchSize(Integer.MIN_VALUE);
ps.setString(1, "active");
ResultSet rs = ps.executeQuery(); // 此时连接保持打开,数据未全量传输
MyBatis 中用 Cursor 替代 List
如果你用的是 MyBatis(尤其 MyBatis-Plus),避免写 List<User> users = mapper.selectAll()。应定义返回类型为 Cursor<User>:
立即学习“Java免费学习笔记(深入)”;
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- Mapper 接口方法声明:
Cursor<User> selectAllActiveUsers(); - XML 中 SQL 不变,但 MyBatis 会自动适配底层流式行为(前提是 DataSource 和驱动已正确配置)
- 使用时遍历 Cursor,每取一条就写入 Excel,用完即丢,不缓存
注意:Cursor 是惰性迭代器,必须在数据库连接未关闭前完成遍历;遍历结束后建议显式 close(尤其在 try-with-resources 中)。
Excel 写入必须用流式引擎
即使查询是流式的,若用传统 XSSF 或 EasyExcel 的 list 写法,仍会把全部数据先塞进内存再生成文件。必须选用支持“逐行写入”的模式:
- Apache POI SXSSF:内部基于临时文件 + 滚动窗口,默认只保留 100 行在内存,其余刷盘
-
EasyExcel 3.x:调用
EasyExcel.write(outputStream).sheet().doWrite(new SimpleWriterHandler() {...}),传入自定义 WriterHandler 实现逐条写入 - 避免任何
write(List<>)调用,那等于又回到 OOM 路线
事务与连接生命周期要手动管理
流式查询期间,数据库连接不能被连接池回收,否则 ResultSet 会失效。因此:
- 不要在 Spring @Transactional 方法里直接用 Cursor —— 事务可能超时或提前提交,导致连接关闭
- 推荐手动获取 Connection,用 try-with-resources 包裹整个流程(包括 ResultSet 和 OutputStream)
- 务必确保 ResultSet 完全遍历完毕后再 close,否则剩余数据无法读取,且连接可能泄露
简单说:一次导出 = 一个长连接 + 一个游标 + 一个流式写入器,三者生命周期对齐,缺一不可。

















