ResultSet不关闭会导致OOM,流式查询需显式设置fetchSize并用try-with-resources确保Connection、Statement、ResultSet三级关闭,避免内存泄漏和连接超时。

ResultSet不关会OOM,流式查询必须显式关闭
Java里用Statement或PreparedStatement查几百万行数据时,驱动默认把整张结果集缓存在JVM堆里——哪怕你只调一次next(),ResultSet背后可能已加载全部数据。MySQL Connector/J 8.0+ 默认启用useCursorFetch=true,但前提是fetchSize > 0且ResultSet.TYPE_FORWARD_ONLY,否则照样全量加载。
- 必须手动设置
statement.setFetchSize(Integer.MIN_VALUE)(MySQL)或Integer.MIN_VALUE(PostgreSQL)触发服务器端游标 - 必须用
try-with-resources确保ResultSet、Statement、Connection三级都关闭,漏关任意一层都可能让连接和结果集长期驻留 - 别在循环里反复
executeQuery()——每次新建Statement却没关旧的,内存泄漏比数据本身还快
分页拉取不是加LIMIT OFFSET就完事
传统SELECT * FROM t ORDER BY id LIMIT 10000, 100在偏移量大时性能断崖下跌,因为MySQL仍要扫描前10000行。真正可行的是基于游标(cursor-based pagination):用上一页最后一条记录的id作为下一页起点。
- 要求排序字段严格唯一且有索引,
ORDER BY created_at, id比单created_at更稳妥 -
WHERE created_at > ? OR (created_at = ? AND id > ?)这种复合条件才能跳过已读数据,避免漏行或重复 - 别依赖
OFFSET做后台导出——100万行分页时,第999页的OFFSET 99900会让执行计划退化成全表扫描
流式处理中ResultSet.next()阻塞是正常现象
启用流式查询后,ResultSet.next()第一次调用会发起网络请求并等待首行返回;后续调用才真正“流式”——但每行仍是按需从TCP缓冲区解包。如果网络卡顿或数据库慢,next()就会阻塞,这不是代码写错了。
- 检查
socketTimeout配置(如MySQL的connectTimeout和socketTimeout),避免无限等待 - 不要在
while (rs.next())里做耗时操作(比如远程HTTP调用),否则流式优势归零,还可能触发数据库连接超时 - PostgreSQL需要额外设
setAutoCommit(false)才能启用流式,MySQL则不需要——驱动行为差异容易踩坑
MyBatis里fetchSize和resultType配合不当照样OOM
MyBatis的<select fetchSize="1000">只是给JDBC层透传setFetchSize,但如果resultType指向一个大对象(比如含@Lob字段的实体),或者用了<collection>嵌套查询,实际内存占用远超行数×对象大小。
- 确认
fetchSize生效:开启MyBatis日志,看是否输出Setting fetch size to 1000 - 避免
resultMap里映射未使用的字段,尤其BLOB/TEXT类型,它们会立即加载进内存 - 流式场景下禁用
cache,二级缓存会把整批结果存入堆,跟流式初衷直接冲突
最常被忽略的一点:数据库连接池配置。即使JDBC层流式正确,若连接池(如HikariCP)的maxLifetime短于大数据查询耗时,连接可能在读取中途被回收,导致SQLException: Connection is closed——这时候问题看似在ResultSet,根子其实在连接生命周期管理上。










