1. 问题现场:Session 关了,游标为什么还在涨
如果你正在维护一套 Hibernate 3.2 + Oracle 10g 的老系统,某天日志里突然开始刷ORA-01000: maximum open cursors exceeded,那基本可以确认:有游标没被释放,而且是被持续累积出来的。
这个报错的字面意思是「打开游标数超过上限」。Oracle 里每个Statement、ResultSet都会占用一个游标,open_cursors默认只有 300。单看 300 好像不少,但一个中等并发的 Java 后端,几轮分页查询、几次批量更新就能把额度吃满。更麻烦的是,它不会在启动时立刻报,而是运行一段时间后才爆发,所以很多人第一反应是「我明明在 finally 里session.close()了啊」。
我见过最典型的场景是这样的:DAO 层用ThreadLocal管理 Session,每个方法 finally 里都老老实实close(),代码 review 看不出问题。但连接来自容器托管的数据源(比如 Tomcat JDBC Pool、C3P0、DBCP),Connection.close()并不是真的关闭物理连接,而是把连接还回池子。JDBC 规范里说「关闭 Connection 应连带关闭它产生的 Statement 和 ResultSet」,但连接池为了复用,往往只做「归还」动作,Statement 缓存还挂在连接上。于是 Hibernate 的Session.close()触发了Connection.close(),可底层 PreparedStatement 没被真正关掉,游标就一直占着。
所以排查这件事,核心不是「你有没有 close」,而是「close 之后游标到底有没有被释放」。这篇就按这个思路走:先确认游标现状,再给可复制的 Hibernate 配置骨架,然后做泄漏验证,最后说清楚排查过程中怎么用 TaoToken 统一 Key 通道管理 AI 辅助调用,避免到处散落 key。
2. 前置准备:用 TaoToken 统一 Key 通道
排查这类问题,你大概率会同时做几件事:查 Oracle 视图、翻 Hibernate 3.2 的老文档、让 AI 帮你解释某段配置、对比不同连接池参数。如果每个工具都单独配一套 key,管理起来很乱,尤其是团队协作时,谁用了哪个 key、额度还剩多少都不清楚。
TaoToken 在这里的角色是「统一 Key 通道」:你申请一个 key,就能在模型对话、编码辅助、API 调用这些场景里复用同一套凭证。对这次排查来说,最实用的两个入口是模型对话和 API Keys 管理。
模型对话入口适合边查边问,比如你把hibernate.cfg.xml片段贴进去,让它帮你确认hibernate.connection.release_mode的取值影响。地址是:
https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
API Keys 管理入口用来创建和轮换 key,排查期间如果多人协作,可以给每人分一个,出问题好定位:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
如果你排查完顺手要写点自动化脚本(比如定时查v$open_cursor并告警),那 Coding Plan 更合适,它面向长期编码和 Agent 场景:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
注意:TaoToken 只是统一管理 AI 调用的 key 通道,不替代你的数据库客户端,也不碰生产库连接。所有 Oracle 操作还是走你自己的 sqlplus 或客户端。
3. 可复制配置:hibernate.cfg.xml 与连接池骨架
先给一份能直接抄的 Hibernate 3.2 配置骨架。重点在release_mode和连接池的 statement 缓存开关,这两处是 ORA-01000 的高发区。
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <property name="hibernate.connection.driver_class">oracle.jdbc.driver.OracleDriver</property> <property name="hibernate.connection.url">jdbc:oracle:thin:@127.0.0.1:1521:orcl</property> <property name="hibernate.connection.username">app_user</property> <property name="hibernate.connection.password">app_pwd</property> <property name="hibernate.dialect">org.hibernate.dialect.Oracle10gDialect</property> <!-- 关键:让 Hibernate 在事务结束后释放连接,而不是一直持有 --> <property name="hibernate.connection.release_mode">after_transaction</property> <!-- 关闭 JDBC 层面的 PreparedStatement 缓存,避免游标挂在池连接上 --> <property name="hibernate.connection.provider_class"> org.hibernate.connection.C3P0ConnectionProvider </property> <property name="hibernate.c3p0.max_statements">0</property> <property name="hibernate.c3p0.max_size">30</property> <property name="hibernate.c3p0.min_size">5</property> <property name="hibernate.c3p0.timeout">1800</property> <!-- 打开 SQL 日志,排查阶段方便定位是哪条语句没释放 --> <property name="hibernate.show_sql">true</property> <property name="hibernate.format_sql">true</property> </session-factory> </hibernate-configuration>几个参数值得单独说。hibernate.connection.release_mode设成after_transaction,意思是事务一结束就把连接还回池子,而不是等 Session 关闭。老系统里很多人用默认的auto,在 JTA 环境下行为不确定,容易拖到 Session 关闭才释放,中间这段时间游标一直占着。
hibernate.c3p0.max_statements设成0是直接关掉 statement 缓存。Hibernate 官方 FAQ 里那条「Oracle JDBC driver doesn't much like to have its prepared statements cached」说的就是这个。缓存本意是提速,但在 Oracle + 老驱动组合下,缓存住的 PreparedStatement 对应的游标不会释放,时间一长就爆。
如果你用的是 DBCP 而不是 C3P0,对应参数是poolPreparedStatements=false,效果一样。下面这张表帮你对照:
| 连接池 | 关闭 statement 缓存参数 | 建议值 |
|---|---|---|
| C3P0 | hibernate.c3p0.max_statements | 0 |
| DBCP | poolPreparedStatements | false |
| Tomcat JDBC | jdbcInterceptors 去掉 StatementCache | 不配置 |
改完配置别急着上生产,先在测试库跑一轮压测,观察游标数变化。
4. 验证请求:查游标、复现泄漏、确认修复
配置改完,得用数据说话。第一步先看当前游标上限和实际占用。
-- 查看 open_cursors 上限 show parameter open_cursors; -- 查看当前会话打开的游标数,按数量倒序 SELECT s.sid, s.serial#, s.username, s.status, COUNT(*) AS cursor_count FROM v$open_cursor oc JOIN v$session s ON oc.sid = s.sid GROUP BY s.sid, s.serial#, s.username, s.status ORDER BY cursor_count DESC;v$open_cursor是排查核心视图,它列出每个会话当前打开的游标。如果某个会话的cursor_count持续上涨不回落,那就是泄漏点。你可以再钻一层,看具体是哪些 SQL:
SELECT oc.sid, oc.sql_id, oc.sql_text FROM v$open_cursor oc WHERE oc.sid = &target_sid ORDER BY oc.sql_id;复现泄漏的动作也很直接:写一个循环调用 DAO 的测试方法,跑 500 次分页查询,每 50 次查一次v$open_cursor。修复前你会看到游标数单调上升,修复后应该稳定在一个小范围内波动。
// 简易复现:循环调用分页查询,观察游标是否累积 public void reproduceCursorLeak(int rounds) { for (int i = 0; i < rounds; i++) { Session session = HibernateUtil.getSession(); Transaction tx = null; try { tx = session.beginTransaction(); Query q = session.createQuery("from Order o order by o.id"); q.setFirstResult(i * 10); q.setMaxResults(10); q.list(); tx.commit(); } catch (Exception e) { if (tx != null) tx.rollback(); throw new RuntimeException(e); } finally { session.close(); } if (i % 50 == 0) { System.out.println("round=" + i + " 检查 v$open_cursor"); } } }跑的时候在另一个 sqlplus 窗口反复执行第 4 节第一条查询,对比cursor_count。修复生效的标志是:循环结束后游标数回落到基线,而不是停在高位。
如果确认是上限太低导致的误报(比如业务确实需要更多游标),可以临时调大:
-- 需要 DBA 权限,scope=both 表示立即生效并写入 spfile ALTER SYSTEM SET open_cursors = 1500 SCOPE = BOTH;但记住,调大只是缓解,不是修复。真正的泄漏点不解决,1500 也会被吃满。
5. 常见错排查:为什么改了配置还在报
错误一:改了max_statements=0但没重启应用。连接池参数是启动时初始化的,热部署不一定生效。老老实实重启,再观察。
错误二:release_mode设了after_transaction,但代码里手动开了 Session 没走事务。比如直接session.createQuery(...).list()而不beginTransaction(),这种情况下连接释放时机不受release_mode控制。排查时用hibernate.show_sql=true配合日志,看每条 SQL 前后有没有对应的连接归还记录。
错误三:游标泄漏不在 Hibernate,而在原生 JDBC 代码里。老系统经常混着写,某段Connection conn = dataSource.getConnection()之后Statement没关,只关了 Connection。这种用v$open_cursor按sql_text一筛就能看出来,跟 Hibernate 无关。
错误四:open_cursors调大了,但v$open_cursor里游标数还在涨。说明泄漏仍在继续,只是暂时没触顶。这时候别放松,继续按第 4 节的方法定位具体会话和 SQL。
错误五:多个数据源共用一个连接池配置。如果应用里有多个SessionFactory,每个都要单独检查release_mode和 statement 缓存参数,漏一个就够爆。
排查过程中如果拿不准某段配置的含义,可以把片段贴到模型对话里问,用同一个 TaoToken key 就行,不用再单独申请:
https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
6. 收尾:把排查动作固化成习惯
这类问题的价值不在「修好一次」,而在「下次能快速定位」。我的做法是把第 4 节那两条v$open_cursor查询存成 sqlplus 脚本,配合一个定时任务,游标数超过阈值就告警。这样不用等 ORA-01000 报出来才发现。
另外,Hibernate 3.2 确实老,但很多存量系统还在跑。与其大动干戈升级,不如先把release_mode和 statement 缓存这两个开关调对,成本低、见效快。如果排查完想写个自动化巡检脚本,用 Coding Plan 那条通道更顺手:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite
最后提醒一句:ALTER SYSTEM SET open_cursors是 DBA 操作,改之前确认好scope,memory重启失效,both才持久化。别在没备份 spfile 的情况下乱调。