报表导出OOM主因是MyBatis默认全量加载百万级结果至List,应改用游标流式处理:声明ResultHandler或Cursor返回类型、XML配置fetchSize、禁用事务、驱动层启用useCursorFetch、Handler内禁止缓存。

直接用 List 接收百万级查询结果,是报表导出 OOM 的最常见原因。MyBatis 默认把整张结果集缓存进内存再封装成 List,哪怕 SQL 很简单,只要数据量上去,JVM 堆立刻告急。解决的核心不是“怎么撑住”,而是“根本别让它进内存”——必须切换到基于游标的流式处理路径。
必须显式启用游标模式
Mapper 接口不能返回 List,必须声明为接收 ResultHandler 或 Cursor:
-
错误写法:
List—— 全量加载,OOM 高发selectAllOrders(); -
正确写法:
void selectAllOrders(ResultHandler或handler); CursorselectAllOrders(); - XML 中对应 SQL 标签需加
@Options(resultSetType = FORWARD_ONLY, fetchSize = -2147483648)(MySQL)或fetchSize="1000"(PostgreSQL/通用场景)
关闭事务与自动提交
Spring 的 @Transactional 会强制开启事务并自动 commit,导致 MyBatis 无法维持游标状态,底层 ResultSet 被提前关闭:
- 导出接口上必须标注
@Transactional(propagation = Propagation.NOT_SUPPORTED) - 手动获取 SqlSession 时,调用
sqlSessionFactory.openSession(ExecutorType.SIMPLE, false),第二个参数false表示不自动 commit - 确认 Spring Boot 配置中
mybatis.configuration.default-executor-type=simple已生效
驱动层必须配合流式参数
MyBatis 只是调度层,真正决定是否流式的是 JDBC 驱动。MySQL Connector/J 必须满足三个条件才启用流式传输:
- 连接 URL 加参数:
?useCursorFetch=true&defaultFetchSize=1000(注意 & 需转义) - 驱动版本 ≥ 5.1.38 或 ≥ 8.0.16(老版本会静默降级)
- 执行后检查
ResultSet.getFetchSize()是否返回1000或Integer.MIN_VALUE,否则配置未生效
ResultHandler 内严禁缓存
流式的意义是“来一条、处理一条、丢一条”。任何在 handler 里 add() 到 ArrayList、HashMap 的行为,都等于回到全量加载的老路:
- Excel 导出用
SXSSFWorkbook,每写 100 行调一次row.flush() - CSV 导出用
BufferedWriter直接写OutputStream,编码设为 UTF-8 - HTTP 响应流用
ResponseEntity.ok().body(new StreamingResponseBody() { ... }),在 lambda 里写 output,只flush()不close()










