1. 从一次 ORA-01000 报错说起:游标到底被谁占满了
应用日志里突然刷出ORA-01000: maximum open cursors exceeded,通常意味着某个会话打开的游标数量超过了open_cursors参数允许的上限。这个报错本身不复杂,麻烦的是它只告诉你“超了”,不告诉你“谁没关”。如果只是把open_cursors从 300 调到 3000,可能撑几天又爆,因为根因往往是代码里Statement/ResultSet没关,或者循环里反复创建游标却不释放。
我处理这类问题的思路是:先用 Errorstack 在报错瞬间把会话的游标现场 dump 下来,看清是哪些 SQL、哪些游标状态卡在 BOUND,再结合v$open_cursor和open_cursors参数判断是配置偏小还是泄漏。下面按“复现报错 → 设置 Errorstack → 抓 trace → 分析游标 → 调整参数 → 验证修复”走一遍,你可以直接照着在测试库上做一次。
适合谁看:Oracle DBA、Java/中间件开发、需要定位连接池游标泄漏的运维同学。核心检索词就是 Errorstack、ORA-01000、open_cursors、游标泄漏。
2. 前置准备:TaoToken 与排查环境
排查过程中如果要用大模型帮你读 trace、解释游标状态,或者让 coding agent 辅助写诊断脚本,可以先把 TaoToken 的接入配好。它提供 OpenAI 兼容接口,模型对话、API Key、Coding Plan 都有独立入口,按需选即可。
- 官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- API 基址:https://taotoken.net/api
- 模型对话(验证模型是否可用):https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite
- Coding Plan(长期编码/Agent 场景):https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite
- 控制台:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
- API Keys:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite
- 接入文档:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
Oracle 侧需要:一个可复现的测试库(11g/12c/19c 均可)、能执行alter system的权限、能访问diag/rdbms/<db>/<inst>/trace目录。Java 侧准备一个故意不关游标的 demo,用来制造泄漏。
3. 可复制配置:复现 ORA-01000 并挂上 Errorstack
3.1 先把 open_cursors 调小,制造报错
为了快速复现,把参数临时调小。生产上不要这么干,测试库随意。
-- 查看当前值 show parameter open_cursors; -- 临时调小到 15,方便复现 alter system set open_cursors=15 scope=both; -- 确认 show parameter open_cursors;scope=both表示内存和 spfile 同时生效,重启后仍是 15。复现完记得改回去。
3.2 设置 Errorstack 事件
Errorstack 的作用是:当指定错误号出现时,自动 dump 错误栈、进程栈和会话游标信息。针对 ORA-01000,事件号就是 1000。
-- 实例级:出现 ORA-01000 时 dump level 3 alter system set events '1000 trace name errorstack level 3'; -- 排查结束后关闭 alter system set events '1000 trace name context off';Errorstack 的级别含义:
| Level | 内容 |
|---|---|
| 1 | 错误堆栈 + 函数调用堆栈 |
| 2 | Level 1 + ProcessState |
| 3 | Level 2 + Context area(显示所有 cursors,重点显示当前 cursor) |
排查游标泄漏用 level 3,因为只有它会把会话打开的游标列表打出来。也可以只在某个会话上设置:
alter session set events '1000 trace name errorstack level 3';3.3 用 Java demo 触发泄漏
下面这段代码在循环里反复createStatement和executeQuery,但只在 finally 里关最后一次的rset/stmt,前面的游标全部泄漏。循环 300 次,open_cursors=15,必然报 ORA-01000。
import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class TestCursor { public static void main(String args[]) throws Exception { Connection con = null; Statement stmt = null; ResultSet rset = null; try { Class.forName("oracle.jdbc.driver.OracleDriver"); String url = "jdbc:oracle:thin:@127.0.0.1:1521:ora11"; con = DriverManager.getConnection(url, "test", "test"); for (int i = 0; i <= 300; i++) { stmt = con.createStatement(); rset = stmt.executeQuery("select * from test"); while (rset.next()) { rset.getString(1); } // 注意:这里没有 close,游标持续累积 } } catch (Exception e) { e.printStackTrace(); } finally { try { if (rset != null) rset.close(); if (stmt != null) stmt.close(); if (con != null) con.close(); } catch (Exception e) { e.printStackTrace(); } } } }运行后会看到类似堆栈:
java.sql.SQLException: ORA-00604: 递归 SQL 级别 1 出现错误 ORA-01000: 超出打开游标的最大数 ORA-00604: 递归 SQL 级别 1 出现错误 ORA-01000: 超出打开游标的最大数 ORA-01000: 超出打开游标的最大数 at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:445) ... at TestCursor.main(TestCursor.java:19)4. 验证请求与成功结果:读 trace 定位泄漏游标
4.1 找到 trace 文件
报错后,alert 日志里会出现类似:
OS Pid: 8588 executed alter system set events '1000 trace name errorstack level 3' Errors in file f:\app\administrator\diag\rdbms\ora11\ora11\trace\ora11_ora_8764.trc: ORA-01000: 超出打开游标的最大数打开ora11_ora_8764.trc,重点看Session Cursor Dump和Session Open Cursors两段。
4.2 解读游标 dump
----- Session Cursor Dump ----- Current cursor: 0, pgadep=0 Open cursors(pls, sys, hwm, max): 15(0, 3, 15, 15) NULL=0 SYNTAX=0 PARSE=0 BOUND=15 FETCH=0 ROW=0Open cursors(...): 15(0, 3, 15, 15)说明当前会话打开了 15 个游标,其中 3 个是系统递归游标,高水位和上限都是 15。BOUND=15表示 15 个游标全部处于 BOUND 状态——已经绑定但没关闭,典型的泄漏特征。
继续往下看:
----- Session Open Cursors ----- Cursor#1(0x000000001BD91998) state=BOUND curiob=0x000000001BDAD5B0 Cursor#5(0x000000001BD91BD8) state=BOUND curiob=0x000000001CBD94E8 ----- Dump Cursor sql_id=c99yw1xkb4f1u xsc=0x000000001CBD94E8 cur=0x000000001BD91BD8 ----- ObjectName: Name=select * from test Cursor#6(0x000000001BD91C68) state=BOUND curiob=0x000000001CBD8958 ----- Dump Cursor sql_id=c99yw1xkb4f1u xsc=0x000000001CBD8958 cur=0x000000001BD91C68 ----- ObjectName: Name=select * from test Cursor#9(0x000000001BD91E18) state=BOUND curiob=0x000000001CBD66A8 ----- Dump Cursor sql_id=c99yw1xkb4f1u xsc=0x000000001CBD66A8 cur=0x000000001BD91E18 ----- ObjectName: Name=select * from test多个游标指向同一个sql_id=c99yw1xkb4f1u,SQL 文本都是select * from test。这说明应用在循环里反复执行同一条 SQL,每次新建游标却不关闭。到这里根因就清楚了:不是open_cursors太小,而是代码泄漏。
4.3 用 v$open_cursor 交叉验证
在报错会话还活着的时候,可以查:
-- 按会话统计打开的游标数 select s.sid, s.serial#, s.username, count(*) as cursor_cnt from v$open_cursor o, v$session s where o.sid = s.sid group by s.sid, s.serial#, s.username order by cursor_cnt desc; -- 看具体是哪些 SQL select sid, sql_id, sql_text from v$open_cursor where sid = <问题会话SID> order by sql_id;如果某个 SID 的游标数接近open_cursors,且 SQL 高度重复,基本可以确认泄漏点。
5. 本篇常见错排查
5.1 Errorstack 设了但没生成 trace
先确认事件是否真的生效:
select name, value from v$parameter where name = 'event'; -- 或 show parameter event;如果没看到1000 trace name errorstack level 3,可能是alter system没执行成功,或者被其他 event 覆盖。另外 trace 目录权限不足也会导致写不进去,检查background_dump_dest和user_dump_dest。
5.2 报错是 ORA-01000 但 trace 里没有 Session Open Cursors
大概率是 level 设成了 1 或 2。只有 level 3 才包含 Context area。改成 level 3 重新触发。
5.3 调大 open_cursors 后不报错了,但连接池还是异常
open_cursors是会话级上限,调大只是延后爆发。如果v$open_cursor里某个会话游标数持续增长不回落,说明泄漏仍在。正确做法是修代码:Statement、PreparedStatement、ResultSet用完即关,推荐 try-with-resources。
try (Connection con = DriverManager.getConnection(url, user, pwd); PreparedStatement ps = con.prepareStatement("select * from test"); ResultSet rs = ps.executeQuery()) { while (rs.next()) { rs.getString(1); } }5.4 参数改了没生效
open_cursors是动态参数,scope=both立即生效。但如果用scope=spfile,需要重启。另外 RAC 环境要在每个实例上确认,或者用sid='*'。
alter system set open_cursors=1000 scope=both sid='*';5.5 排查完忘记关 Errorstack
事件会一直挂在实例上,每次 ORA-01000 都 dump,trace 目录可能被撑爆。排查结束务必执行:
alter system set events '1000 trace name context off';6. 收尾与后续接入
整个链路走下来:调小open_cursors复现 → 挂 Errorstack level 3 → 跑泄漏 demo → 读 trace 看到多个 BOUND 游标指向同一 sql_id → 用v$open_cursor确认 → 修代码用 try-with-resources → 把open_cursors调回合理值。根因是游标泄漏,不是参数太小,这一点在 trace 里看得很清楚。
如果你想让 coding agent 帮你批量扫描项目里没关的Statement,或者用模型解释 trace 里的游标状态,可以走 Coding Plan 和模型对话入口;API Key 和接入文档在下面:
- API Keys:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite
- 接入文档:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
- 模型对话:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite
- Coding Plan:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite
最后提醒一句:生产库上设 Errorstack 前先确认 trace 目录空间,level 3 的 dump 在游标多的时候文件会比较大,别把磁盘写满。