1. 从一次 ORA-01000 报警说起:open_cursor 到底卡在哪
Java 应用跑着跑着突然抛ORA-01000: maximum open cursors exceeded,这个报错在测试环境里特别常见,尤其是批量任务、定时跑批、循环查库的场景。它的本质不是数据库挂了,而是当前会话打开的游标数量超过了open_cursors参数上限。游标在 Oracle 里对应的是服务端为每条 SQL 分配的一块私有内存区域,Statement和ResultSet没关,游标就不会释放,累积到阈值直接报错。
很多人第一反应是去调大open_cursors,比如从 300 改到 2000。这招能续命,但只是把泄漏点往后推,跑得久一点照样爆。真正要做的,是把「谁在开游标、开了多少、哪条 SQL 没关」这三件事查清楚。这篇就按这个思路走:先用 SQL 定位当前会话的游标数量和正在执行的语句,再从连接池配置和代码层两条线索切入,最后把诊断脚本的调用凭证统一收口到 TaoToken 的通道里,避免诊断脚本散落各处、Key 到处硬编码。
适合谁看:正在被 ORA-01000 折磨的 Java 后端、需要排查连接池游标泄漏的 DBA、以及想把诊断工具链统一管理的同学。下面所有 SQL 和配置都可以直接复制到测试环境跑。
2. 前置准备:用 TaoToken 统一管理诊断脚本的调用凭证
排查游标泄漏时,通常会写一堆诊断脚本:有的查v$mystat,有的查v$open_cursor,有的跑压测复现。这些脚本如果各自硬编码数据库连接串、各自申请模型调用的 Key,管理起来会很乱。我的做法是把诊断脚本里需要调用外部模型能力(比如让模型帮忙分析 SQL 文本、生成排查建议)的凭证,统一走 TaoToken 的 API 通道。
TaoToken 在这里的角色是统一入口:一个 Key 管多个模型的调用,诊断脚本不用为每个模型单独配一套凭证。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api ,注意 API 地址不带 UTM 参数。
具体操作路径:先到控制台创建 Key,地址是 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 。如果你只是想让模型帮忙读一段 SQL 文本、给排查方向,用模型对话页就够了: https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 。如果是长期跑编码类诊断脚本、要接 Agent,那更适合用 Coding Plan: https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。
注意:诊断脚本里不要明文写 Key,用环境变量注入。TaoToken 的 Key 只是调用凭证,数据库连接串还是走你自己的配置中心。
接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,ClaudeCode 相关的接入说明在 https://taotoken.net/claudecode-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode-anthropic&utm_campaign=rewrite 。把凭证收口之后,后面所有诊断脚本调用模型分析时,只认一个环境变量,换 Key 只改一处。
3. 可复制配置:连接池参数与 open_cursor 查询 SQL
3.1 先确认数据库侧的 open_cursors 上限
在动手改代码之前,先看当前上限是多少。用 DBA 账号执行:
show parameter open_cursors;输出里value就是上限。测试环境常见是 300,生产可能 1000 到 2000。这个值决定了你有多大的缓冲空间,但不解决泄漏。
3.2 统计当前会话打开的游标数量
这是定位问题的第一步。v$mystat里opened cursors current这个统计项,就是当前会话打开的游标数:
select a.value, a.sid from v$mystat a, v$statname b where a.statistic# = b.statistic# and b.name = 'opened cursors current';a.value是打开数量,a.sid是当前会话 ID。把这个 sid 记下来,下一步要用。如果这个值在循环里持续上涨、不回落,基本可以确认有泄漏。
3.3 查出该会话正在执行的 SQL 文本
拿到 sid 之后,去v$open_cursor里查这个会话打开了哪些游标:
select sql_text from v$open_cursor where sid = &sid;把上一步的 sid 填进去。结果里会列出该会话当前打开的所有 SQL 文本。重点看那些重复出现、或者明显是循环里执行的查询。比如一个select ... from t where id = ?出现几十次,那大概率是循环里创建了 Statement 没关。
3.4 连接池侧的关键参数
连接池配置不当会放大游标泄漏。以 HikariCP 为例,几个参数要盯紧:
spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 leak-detection-threshold: 20000leak-detection-threshold是关键,设成 20000 毫秒(20 秒),连接借出超过这个时间没归还,HikariCP 会打警告日志,能帮你抓到没关连接的代码位置。max-lifetime要小于数据库侧的连接空闲超时,避免拿到已被服务端断开的连接。
如果用 Druid,对应的是:
spring: datasource: druid: max-active: 20 initial-size: 5 max-wait: 30000 remove-abandoned: true remove-abandoned-timeout: 180 log-abandoned: trueremove-abandoned开启后,连接借出超过remove-abandoned-timeout秒没归还,会被强制回收并打日志。这个日志就是泄漏点的直接线索。
3.5 用 TaoToken 通道跑诊断脚本的调用示例
诊断脚本里如果需要调用模型分析 SQL 文本,用环境变量拿 Key,请求走 TaoToken 的 API:
export TAOTOKEN_API_KEY="你的Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"import os import requests api_key = os.environ["TAOTOKEN_API_KEY"] base_url = os.environ["TAOTOKEN_BASE_URL"] resp = requests.post( f"{base_url}/v1/chat/completions", headers={"Authorization": f"Bearer {api_key}"}, json={ "model": "claude-sonnet-4-20250514", "messages": [ {"role": "user", "content": "分析这段SQL是否有游标泄漏风险:select * from t where id = ?"} ] }, timeout=30 ) print(resp.json())这样诊断脚本的凭证只从环境变量来,换 Key 不用改代码。
4. 验证请求:复现游标泄漏并逐条关闭资源
4.1 写一个会泄漏的最小复现程序
先故意写一段不关 ResultSet 和 Statement 的代码,在测试环境复现 ORA-01000:
public void leakCursors(Connection conn) throws Exception { for (int i = 0; i < 500; i++) { Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("select * from dual"); // 故意不关 rs 和 stmt } }跑这段,循环几百次之后,再执行 3.2 的查询,会看到opened cursors current的值一路涨到接近open_cursors上限,然后抛 ORA-01000。
4.2 改成正确关闭的版本
用 try-with-resources 保证 ResultSet 和 Statement 一定关闭:
public void safeQuery(Connection conn) throws Exception { String sql = "select * from dual"; try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { while (rs.next()) { // 处理结果 } } }如果项目还在用老式 try-catch-finally,那 finally 里必须按 ResultSet、Statement、Connection 的顺序逐层关闭,且每个 close 都要单独 try-catch,避免前一个关闭失败导致后面不执行:
Statement stmt = null; ResultSet rs = null; try { stmt = conn.createStatement(); rs = stmt.executeQuery("select * from dual"); while (rs.next()) { } } finally { if (rs != null) { try { rs.close(); } catch (SQLException e) { log.warn("close rs", e); } } if (stmt != null) { try { stmt.close(); } catch (SQLException e) { log.warn("close stmt", e); } } }4.3 验证关闭效果
改完之后重新跑压测,再执行 3.2 的查询。正常情况下,opened cursors current会在每次查询结束后回落到一个稳定值,不会持续上涨。如果还是涨,说明还有别的泄漏点,回到 3.3 查v$open_cursor,看哪条 SQL 还在重复出现。
4.4 用连接池日志交叉验证
开启 HikariCP 的leak-detection-threshold后,如果还有连接没归还,日志里会出现类似:
Connection leak detection triggered for conn0, stack trace follows java.lang.Exception: Apparent connection leak detected at com.example.YourDao.query(YourDao.java:42)这个堆栈直接指向没关连接的代码行,比翻代码快得多。
5. 本篇常见错排查
5.1 查 v$open_cursor 返回空
v$open_cursor需要相应权限,普通业务账号可能查不到。用 DBA 账号,或者给业务账号授select on v_$open_cursor。另外 sid 填错也会返回空,确认 3.2 里拿到的 sid 是当前会话的。
5.2 open_cursors 调大了还是报错
调大只是提高上限,泄漏速度不变的话,只是报错来得晚一点。必须回到代码层找没关的 ResultSet 和 Statement。重点查循环体、异常分支、提前 return 的路径,这些地方最容易漏关。
5.3 连接池 remove-abandoned 没生效
Druid 的remove-abandoned依赖remove-abandoned-timeout,单位是秒,设太小会误杀正常长查询,设太大抓不到泄漏。建议测试环境设 180 秒,配合log-abandoned: true看日志。HikariCP 对应的是leak-detection-threshold,单位毫秒,设 20000 比较合适。
5.4 诊断脚本的 Key 报 401
先确认环境变量TAOTOKEN_API_KEY有没有正确导出,再确认请求头是Authorization: Bearer <key>。如果用的是 Coding Plan 或 ClaudeCode 接入,注意 base_url 和模型名要跟文档一致,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。模型对话页可以直接验证 Key 是否可用: https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 。
5.5 游标数在连接归还后不降
Oracle 的游标是会话级的,连接归还到池里但会话没断,游标可能还挂着。如果连接池的max-lifetime设得太大,连接长期不重建,游标就一直累积。把max-lifetime设成小于数据库侧的空闲超时,让连接定期重建,能缓解这个问题。但根治还是靠代码层正确关闭。
6. 把诊断链路收口到统一通道
排查 ORA-01000 这件事,拆开看就是三步:用 SQL 定位游标数量和 SQL 文本,用连接池参数和日志抓泄漏点,用代码层 try-with-resources 或 finally 逐条关闭资源。这三步里,诊断脚本会越来越多,凭证管理容易失控。
我的做法是把所有需要调用模型能力的诊断脚本,统一走 TaoToken 的 API 通道,Key 从环境变量注入,换 Key 只改一处。控制台建 Key 在 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,Key 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。长期跑编码类诊断、要接 Agent 的,用 Coding Plan 更合适: https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。接入细节看文档: https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
最后留一个我踩过的坑:v$open_cursor里看到的 SQL 文本可能被截断,长 SQL 看不全。这时候可以结合v$sql按sql_id查完整文本,或者直接在代码里搜 SQL 片段。定位到具体方法之后,优先检查循环和异常分支,这两个地方是游标泄漏的重灾区。