MyBatis + Oracle 流式读取必须设 fetchSize = Integer.MIN_VALUE(-2147483648),而非关闭;需配合 TYPE_FORWARD_ONLY、CONCUR_READ_ONLY 及正确驱动参数(如 ojdbc8、SetBigStringTryClob=true)才能触发逐行流式,否则仍会全量加载导致 OOM。

MyBatis + Oracle 流式读取必须关掉 fetchSize 吗?
不是必须关,而是必须显式设为 Integer.MIN_VALUE(即 -2147483648),否则 MyBatis 默认用 JDBC 的 Statement 模式批量拉取,内存照样爆。Oracle 驱动对流式支持依赖两个关键点:JDBC URL 中启用 oracle.jdbc.defaultRowPrefetch=1(仅辅助),以及 ResultSet 必须用 ResultSet.TYPE_FORWARD_ONLY + ResultSet.CONCUR_READ_ONLY + fetchSize = Integer.MIN_VALUE 组合触发底层的「逐行流式」行为。
常见错误现象:OutOfMemoryError: Java heap space 即使只 select 1 列;或日志里看到 Row prefetch size: 10 却没生效——本质是 MyBatis 没把 fetchSize 透传给 Oracle JDBC。
- MyBatis XML 中必须在
标签加属性:fetchSize="-2147483648" - Mapper 接口方法不能加
@Options注解覆盖,它不支持负数fetchSize(MyBatis 3.4+ 仍会转成正数) - Spring Boot 配置
mybatis.configuration.default-fetch-size对负值无效,必须写死在 SQL 定义里
Oracle JDBC 驱动版本和连接参数怎么配才不翻车?
Oracle 12c+ 推荐用 ojdbc8.jar(对应 JDK 8+),ojdbc6 在流式场景下有已知 bug:即使设了 fetchSize = Integer.MIN_VALUE,驱动仍可能缓存多行。连接 URL 必须带 SetBigStringTryClob=true 和 oracle.jdbc.autoCommitSpecCompliant=false,前者防 CLOB 字段触发隐式 fetch,后者避免 auto-commit 干扰流式游标生命周期。
典型错误配置:jdbc:oracle:thin:@//host:1521/orcl?useUnicode=true&characterEncoding=utf8 —— 这是 MySQL 风格参数,Oracle 会静默忽略,导致流式失效。
- 正确 URL 示例:
jdbc:oracle:thin:@//10.0.1.100:1521/ORCLPDB1?oracle.jdbc.defaultRowPrefetch=1&SetBigStringTryClob=true&oracle.jdbc.autoCommitSpecCompliant=false - Driver class 必须是
oracle.jdbc.driver.OracleDriver(ojdbc8已废弃该类,改用oracle.jdbc.OracleDriver,但流式兼容性更稳) - 连接池(如 HikariCP)需禁用连接测试:
connection-test-query=SELECT 1 FROM DUAL可能提前消耗游标
流式读取时 MyBatis 的 ResultHandler 怎么写才安全?
ResultHandler 是唯一安全的流式消费方式,绝不能用 List 接收结果——那等于全量加载到内存。它的核心约束是:处理逻辑必须轻量、无状态、不缓存原始对象;一旦 handler 方法返回,MyBatis 立即释放当前行的 ResultSet 资源。
容易踩的坑:resultHandler.handleResult(...) 里调用 session.insert(...) 触发新查询,会破坏当前游标;或把 ResultContext 里的对象塞进全局 List,导致 GC 不掉。
- handler 内禁止任何数据库操作(包括同 session 的其他 select/insert)
- 字段映射尽量用基本类型或 String,避免嵌套对象(如
),否则 MyBatis 可能预加载关联数据 - 示例安全写法:
sqlSession.select("selectBigTable", params, context -> { MyRecord r = (MyRecord) context.getResultObject(); // 立即写入文件 / 发 Kafka / 计算聚合,不保留引用 writer.write(r.getId() + "," + r.getName()); });
为什么 Oracle 游标没关闭、连接被占满?
流式读取本质是保持打开的 ResultSet 和底层 Oracle 游标(cursor),直到 handler 执行完毕或发生异常。如果 handler 抛异常、或业务逻辑卡住(如网络 IO 阻塞)、或未显式调用 sqlSession.close(),游标不会自动释放,数据库端 v$open_cursor 持续增长,最终报 ORA-01000: maximum open cursors exceeded。
这不是 MyBatis 的 bug,而是 JDBC 规范要求:流式 ResultSet 生命周期由应用控制。
- 必须用 try-with-resources 包裹
SqlSession(确保 close) - handler 内部必须做超时控制(如
CompletableFuture.orTimeout()),防无限等待 - Oracle 端监控关键视图:
SELECT sid, sql_id, open_cursor_count FROM v$session WHERE open_cursor_count > 50
亿级数据流式不是“开个开关就完事”,关键是把 ResultSet 当作一次性的、不可重入的资源管道,任何环节持有引用、延迟释放、跨线程传递,都会让整个链路卡死在数据库侧。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











