1. 从一次线上告警说起:ORA-01000 到底在报什么
如果你在 Java 应用日志里看到ORA-01000: maximum open cursors exceeded,紧接着又出现递归 SQL 级别 1 出现错误,那基本可以确定:当前会话打开的游标数量已经超过了数据库允许的上限。ORA-01000 是 Oracle 抛出的明确信号——某个会话持有的游标数突破了OPEN_CURSORS参数设定的阈值。而“递归 SQL 级别 1”通常是数据库内部在执行解析、权限检查等递归操作时,也需要申请游标,结果同样被拒绝,于是把底层错误一并抛了出来。
这个报错最典型的触发场景,就是在循环里反复prepareStatement却不关闭。很多同学写批量更新时习惯这样写:
for (int i = 0; i < balancelist.size(); i++) { prepstmt = conn.prepareStatement(sql[i]); prepstmt.setBigDecimal(1, nb.getRealCost()); prepstmt.setString(2, adclient_id); prepstmt.setString(3, daystr); prepstmt.setInt(4, ComStatic.portalId); prepstmt.executeUpdate(); }循环体里每次conn.prepareStatement()都会在数据库端打开一个游标,但代码从头到尾没有close()。如果balancelist有几千条,游标数就会一路飙升。更隐蔽的是,当你使用连接池时,conn.close()只是把连接归还池中,并不会物理断开,之前未关闭的PreparedStatement和ResultSet仍然占着游标资源。时间一长,游标只增不减,ORA-01000 必然出现。
这篇内容适合正在被 ORA-01000 困扰的后端开发、DBA 和运维同学。我会从“怎么查当前游标占用”开始,一步步带你定位根因,再给出OPEN_CURSORS调整、连接池配置和代码修复的完整方案。你不需要一开始就改数据库参数,先看清楚是谁在占游标,比盲目调大参数有用得多。
2. 动手之前:用 TaoToken 快速验证 SQL 与排查思路
排查 ORA-01000 的过程中,经常需要临时验证一段 SQL 的写法、确认某个视图字段的含义,或者让模型帮你解释一段递归 SQL 的报错上下文。这时候如果手边没有顺手的对话工具,来回切换会比较打断节奏。我平时会用 TaoToken 的模型对话来辅助这类排查,把报错原文和表结构贴进去,让它帮我梳理可能的游标泄漏点,再结合数据库查询去验证。
TaoToken 是一个聚合多种大模型能力的平台,适合需要频繁做技术问答、SQL 解释和代码审查的场景。你可以通过官网了解整体能力,模型对话入口可以直接用来做排障问答。对于长期写代码、跑 Agent 的同学,Coding Plan 更适合持续性的编码任务。下面先把接入需要的东西准备好。
2.1 获取 API Key 与接入信息
无论你是想用模型对话辅助排查,还是把能力接进自己的脚本,第一步都是拿到 API Key。操作路径很直接:
- 打开官网
https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,注册并登录。 - 进入控制台
https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite。 - 在 API Keys 页面
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite创建一个新的 Key,复制保存。
API 的基础地址是https://taotoken.net/api,注意这个地址不带 UTM 参数,直接用于代码里的base_url配置。如果你用的是 Claude Code 这类编码工具,可以参考 ClaudeCodeAnthropic 的接入说明;需要查文档就去 doc 页面。这些入口在排障时用来快速问一句“这个游标查询为什么没结果”,比翻手册快。
注意:API Key 只用于你自己的调用,不要写进前端代码或提交到公开仓库。排查 SQL 时也不要把生产库的敏感连接信息贴给任何外部服务。
3. 可复制配置:查游标、调参数、改连接池
这一节是核心操作区。我按“先观测、再调整、后修复”的顺序给出可以直接复制的语句和配置。你可以在测试库先跑一遍,确认效果后再上生产。
3.1 查询当前 OPEN_CURSORS 参数值
先确认数据库当前允许的最大游标数。缺省值通常是 50,很多老库即使调过也可能只有 300,对稍大的应用来说偏小。
show parameter open_cursors;输出类似:
NAME TYPE VALUE ------------------------------------ ----------- ------------------------------ open_cursors integer 300如果这个值是 300,而你的应用单会话游标峰值经常到几百,那报错就不奇怪了。但记住,调大它只是缓解,不是根治。
3.2 按会话统计打开的游标数
这是定位“谁在占游标”的关键查询。它按会话分组,降序排列,能一眼看出哪个 SID 游标数异常。
select o.sid, s.osuser, s.machine, count(*) num_curs from v$open_cursor o, v$session s where o.sid = s.sid group by o.sid, s.osuser, s.machine order by num_curs desc;输出示例:
SID OSUSER MACHINE NUM_CURS ----- -------- --------- -------- 217 app m1 1000 96 app m2 10 411 app m3 10 50 test local 9SID 217 占了 1000 个游标,基本就是它了。注意v$open_cursor跟踪的是已解析且未关闭的游标,包括通过dbms_sql.open_cursor()打开的动态游标。它不会跟踪那些已打开但未解析的动态游标,不过日常应用里这种情况不多。
3.3 查出具体是哪些 SQL 在占游标
拿到异常 SID 后,用它去关联v$sql,就能看到具体 SQL 文本,反向定位代码位置。
select q.sql_text from v$open_cursor o, v$sql q where q.hash_value = o.hash_value and o.sid = 217;输出会列出该会话当前打开的 SQL,比如:
SQL_TEXT ---------------------------------------- select * from empdemo where empid='212' select * from empdemo where empid='321' select * from empdemo where empid='947'如果看到大量结构相同、只有参数不同的 SQL,而且数量成百上千,那几乎可以确定是循环里创建PreparedStatement没关闭。
3.4 调整 OPEN_CURSORS 参数
确认需要临时放宽上限时,可以动态调整。这个参数修改后立即生效,不需要重启实例。
alter system set open_cursors = 1000;执行后提交:
commit;再确认:
show parameter open_cursors;值变成 1000 即可。需要说明的是,OPEN_CURSORS设置得比实际需要大,并不会显著增加系统开销,所以适当留余量是合理的。但如果你发现调到 1000 后过一阵又报错,那说明泄漏问题没解决,必须回到代码层。
3.5 连接池配置片段
使用连接池时,Connection.close()只是归还连接,不会释放游标。所以连接池层面要确保语句缓存和游标管理配合好。以常见的 HikariCP 为例,可以这样配置:
spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 pool-name: OracleHikariPool如果你用的是 Druid,注意maxPoolPreparedStatementPerConnectionSize这个参数,它控制每个连接缓存的 PreparedStatement 数量。设得过大而代码又不关闭,反而会加剧游标占用:
spring: datasource: druid: max-active: 20 max-pool-prepared-statement-per-connection-size: 20 pool-prepared-statements: true关键点:连接池的语句缓存是“复用”语义,前提是你的代码正确关闭了语句。如果代码不关闭,缓存池也救不了你。
3.6 代码层修复:把 close 放对位置
回到开头那段问题代码,正确写法是在每次执行后关闭PreparedStatement:
for (int i = 0; i < balancelist.size(); i++) { PreparedStatement prepstmt = null; try { prepstmt = conn.prepareStatement(sql[i]); prepstmt.setBigDecimal(1, nb.getRealCost()); prepstmt.setString(2, adclient_id); prepstmt.setString(3, daystr); prepstmt.setInt(4, ComStatic.portalId); prepstmt.executeUpdate(); } finally { if (prepstmt != null) { prepstmt.close(); } } }更好的做法是把prepareStatement提到循环外,用同一个语句反复设置参数执行:
PreparedStatement prepstmt = conn.prepareStatement(sql); for (int i = 0; i < balancelist.size(); i++) { prepstmt.setBigDecimal(1, nb.getRealCost()); prepstmt.setString(2, adclient_id); prepstmt.setString(3, daystr); prepstmt.setInt(4, ComStatic.portalId); prepstmt.addBatch(); } prepstmt.executeBatch(); prepstmt.close();这样游标只打开一次,批量执行完再关闭,效率高且不会泄漏。
4. 验证请求:确认游标真的被释放了
改完代码或参数后,不能只看“没报错”就完事,要主动验证游标是否被正确释放。这里给一个可复现的测试思路。
4.1 用 JDBC 测试 ResultSet 与游标的关系
很多人以为executeQuery返回时结果集已经全部取回内存,游标就关了。实际上ResultSet更像一个指针,next()时才从数据库拉数据,游标在ResultSet关闭前一直存在。下面这段测试代码可以验证:
public class StatementTest extends Thread { private Connection conn; public StatementTest(Connection conn) { this.conn = conn; start(); } public void run() { try { String strSQL = "SELECT * FROM TestTable"; Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(strSQL); int i = 0; while (rs.next()) { System.out.println("----" + i + "------"); i = i + 1; Thread.sleep(5000); } rs.close(); System.out.println("resultset has closed"); Thread.sleep(10000); stmt.close(); System.out.println("statement has closed"); } catch (Exception e) { e.printStackTrace(); } } }运行期间,在 SQLPlus 里执行:
select sql_text from v$open_cursor where sid = 35;你会发现在ResultSet循环期间,这条 SQL 一直出现在v$open_cursor里,说明游标没关。只有rs.close()之后才释放。这验证了:只要 ResultSet 还在用,游标就占着。所以如果你在循环里查询且不关闭 ResultSet,同样会累积游标。
4.2 验证修复效果
修复代码后,重新跑一遍批量操作,然后在操作前后分别执行 3.2 的会话游标统计查询。正常情况下,操作结束后该会话的num_curs应该回落到个位数。如果仍然居高不下,说明还有未关闭的语句或结果集。
你也可以在应用侧加一段监控,定期打印连接池活跃连接数和对应会话的游标数,形成趋势图。一旦发现某会话游标数持续上涨,就能提前告警,而不是等 ORA-01000 爆出来。
5. 本篇常见错排查
排查 ORA-01000 时,有几个坑很容易踩,我逐个列出来。
第一个坑:只调大 OPEN_CURSORS 不查代码。这是最常见的。参数调到 1000、2000,短期不报错了,但游标泄漏还在继续,过几天又炸。正确顺序永远是先查v$open_cursor定位泄漏点,再决定是否调参数。
第二个坑:以为 conn.close() 就释放了游标。在连接池环境下,conn.close()只是归还连接,PreparedStatement和ResultSet如果没关,游标依然被持有。必须显式关闭语句和结果集,或者用 try-with-resources 保证释放。
第三个坑:查询结果集很大时忘记关 ResultSet。有人只关了Statement,没关ResultSet。虽然关闭Statement通常会连带关闭其ResultSet,但依赖这个行为不够稳妥,显式关闭更安全。
第四个坑:递归 SQL 级别 1 出现错误被误判为独立问题。它往往只是 ORA-01000 的伴随现象。数据库内部递归操作也需要游标,主游标耗尽后递归操作同样失败,于是抛出这个错误。解决主问题后它自然消失。
第五个坑:Druid 的语句缓存参数设太大。max-pool-prepared-statement-per-connection-size设得过高,而代码又不关闭语句,会导致每个连接缓存大量语句,游标占用反而更严重。这个值要结合业务实际,不是越大越好。
第六个坑:在循环里 createStatement。和 prepareStatement 一样,createStatement也会打开游标。任何在循环内创建语句的写法都要警惕,尽量提到循环外。
如果你在排查时拿不准某段递归 SQL 的上下文,可以把报错和 SQL 片段丢给模型对话让它帮你分析调用链,再结合v$open_cursor的实际数据交叉验证。需要长期做这类代码审查和排障的,Coding Plan 会更顺手。
6. 把排查清单固化下来
ORA-01000 的排查其实有一套固定动作:先show parameter open_cursors看上限,再查v$open_cursor按会话排序找异常 SID,接着关联v$sql看具体 SQL,然后回到代码检查循环内是否有未关闭的PreparedStatement或ResultSet,最后才是按需调整OPEN_CURSORS和连接池参数。这套流程走下来,绝大多数游标泄漏都能定位。
我自己的习惯是在应用里加一个定时任务,每隔几分钟采样一次各会话游标数,超过阈值就记日志。这样不用等报错,就能提前发现缓慢泄漏。另外,批量操作尽量用addBatch+executeBatch,把语句创建提到循环外,既减少游标又提升性能。
如果你想把这类排查问答和代码审查接进日常工具链,可以从 API Keys 页面创建一个 Key,配合接入文档把模型对话能力接到自己的脚本里。排障时问一句、验证时跑一段,比纯靠记忆翻文档高效得多。