流式查询是解决jdbc海量数据oom的根本方案:必须同时满足usecursorfetch=true、type_forward_only+concur_read_only、setfetchsize(integer.min_value)三条件,否则静默退化为全量加载。

Java JDBC 中用流式查询防内存撑爆,核心是让数据“边拉边处理、用完即丢”,而不是一次性全加载进堆内存。关键不在调大 JVM 堆,而在改掉加载方式。
必须满足的三个硬性条件
缺一不可,否则会静默退化为全量加载:
- 连接 URL 加 useCursorFetch=true,例如:
jdbc:mysql://host:3306/db?useCursorFetch=true - 创建 PreparedStatement 时指定结果集类型:TYPE_FORWARD_ONLY + CONCUR_READ_ONLY
- 执行前调用 setFetchSize(Integer.MIN_VALUE)(即 -2147483648),设成 1000、-1 或其他值都无效
代码写法要避开常见陷阱
不是设了 fetchSize 就自动流式——驱动在预编译阶段若未绑定参数就调 setFetchSize,部分 MySQL 8.0+ 驱动会忽略;务必保证:
- 参数绑定(
pstmt.setString(1, "active"))完成后,再调setFetchSize() -
executeQuery()必须在setFetchSize()之后、且在ResultSet使用前 - ResultSet 必须显式 close(推荐 try-with-resources),否则游标不释放,连接被长期占用
MyBatis 场景下的特别注意点
MyBatis 默认关闭流式,需主动开启并规避干扰行为:
- MappedStatement 中设置 fetchSize="-2147483648"(XML 写法或注解中指定)
- 禁用 autoMappingBehavior=FULL 等可能触发全字段映射的行为
- 避免使用
resultType自动映射复杂对象,优先用ResultHandler手动逐行处理 - 不要混用分页插件(如 PageHelper),它会覆盖 fetchSize 并强制加载全部结果
为什么不能靠 LIMIT OFFSET 解决?
LIMIT OFFSET 只适合前端展示分页,对后端批量处理完全不适用:
- OFFSET 越大,MySQL 越要扫描跳过前面所有行,性能断崖下跌
- 每次查询仍会把本页数据全载入 JVM 堆,百万级数据反复查几十页,内存照样爆
- 无法保证数据一致性(中间有增删改时,分页结果会漏或重)
流式查询让内存稳定在 KB 级,不是技巧,而是 JDBC 游标机制的正确打开方式。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











