1. 从一次保存失败说起:ORA-00604 为什么总跟 ORA-01000 一起出现
线上系统保存一条业务数据,接口直接抛java.sql.SQLException: ORA-00604: 递归 SQL 级别 1 出现错误,堆栈往下翻还能看到ORA-01000: 超出打开游标的最大数。很多人第一反应是「游标不够,调大 open_cursors 就完事」,但如果你只改参数不查根因,过几天同样的报错还会回来。
先说清楚这两个错误的关系。ORA-00604 是 Oracle 在递归 SQL(数据库内部为了执行你的语句而自己发起的 SQL)里出错了,它本身是个「外壳错误」,真正的原因藏在它下面那行。ORA-01000 才是内核:当前会话打开的游标数超过了open_cursors限制。递归 SQL 之所以被牵扯进来,是因为很多内部操作(解析、权限校验、触发器、审计)也要占游标,一旦游标池见底,最先崩的就是这些递归调用。
所以排查思路是两层:先定位是谁把游标耗光了,再决定 open_cursors 调到多少、连接池怎么配。这篇面向 Java 后端和 DBA,给一套能直接复制的查询、调整 SQL 和连接池参数,最后附上 TaoToken 统一 Key/API 通道的config.toml配置骨架,方便你把模型调用和数据库排查脚本放在同一套配置体系里管理。
适合谁看:正在被 ORA-00604/ORA-01000 反复折磨的后端同学、需要给应用定游标上限的 DBA、以及想把排查动作固化成脚本的人。
2. 前置准备:确认版本、权限与 TaoToken 通道
动手前先确认三件事,能省掉后面一半的返工。
第一,确认数据库版本和当前会话能查动态性能视图。v$open_cursor、v$session、v$sql这些视图普通业务账号通常没权限,排查阶段建议用有SELECT ANY DICTIONARY或 DBA 角色的账号,生产上临时授权、查完回收。
-- 确认版本,不同版本 v$open_cursor 字段略有差异 SELECT banner FROM v$version; -- 确认当前用户是否有权限查会话与游标视图 SELECT COUNT(*) FROM v$open_cursor;第二,确认应用侧连接池类型和版本。HikariCP、Druid、Tomcat JDBC 对游标的处理方式不同,后面配置片段会分别给。
第三,如果你打算把排查脚本、模型调用统一走一个 API 通道,可以先把 TaoToken 的 Key 准备好。它的作用是给多个模型/工具提供一个统一的 Key 和 API 入口,省得每个服务各配一套密钥。注册入口在官网 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_medium=csdn&utm_campaign=rewrite&utm_content= 创建 Key,API 基址是 https://taotoken.net/api (这个地址不加 UTM 参数)。Key 生成后先别急着写进代码,第 4 节会给完整的config.toml骨架和连通性验证。
注意:数据库排查脚本里不要硬编码任何密钥,统一从环境变量或配置文件读取,避免 Key 跟着 SQL 脚本一起进版本库。
3. 可复制配置:从 open_cursors 查询到连接池参数
3.1 查当前游标上限和实际占用
先看上限,再看谁在用。这两步顺序不能反,否则你调完参数也不知道有没有生效。
-- 查看当前 open_cursors 上限 SHOW PARAMETER open_cursors; -- 或者用视图查,方便脚本化 SELECT name, value, isdefault FROM v$parameter WHERE name = 'open_cursors';接着定位游标消耗大户。下面这条按会话统计当前打开的游标数,降序排列,排在前面的就是嫌疑对象。
SELECT s.sid, s.serial#, s.username, s.program, s.machine, COUNT(*) AS cursor_cnt FROM v$open_cursor o JOIN v$session s ON o.sid = s.sid GROUP BY s.sid, s.serial#, s.username, s.program, s.machine ORDER BY cursor_cnt DESC;如果某个 JDBC 连接对应的会话游标数几百上千,基本可以锁定是应用没关游标(Statement/ResultSet没 close)或者连接池把游标缓存开太大。
3.2 调整 open_cursors
确认是上限太低而不是泄漏后,再调参数。scope=both表示内存和 spfile 同时改,重启也保留。
-- 临时+持久调整,建议按业务峰值留 2~3 倍余量 ALTER SYSTEM SET open_cursors = 1000 SCOPE = BOTH; -- 验证 SHOW PARAMETER open_cursors;调完不需要重启实例,新会话立即生效,老会话在重连后生效。这里有个坑:open_cursors是每会话上限,不是全局总数。1000 意味着单个会话最多开 1000 个游标,如果你有 200 个连接,理论上限是 20 万,但实际受sessions和 PGA 限制,别盲目往大了调。
3.3 JDBC 连接池参数片段
光调数据库不够,连接池侧的游标缓存和连接数才是泄漏高发区。下面给 HikariCP 和 Druid 两套。
HikariCP:
spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 # 关键:Oracle 下建议关闭语句缓存或设小,避免游标堆积 >spring: datasource: druid: initial-size: 5 max-active: 20 min-idle: 5 max-wait: 30000 # 关闭游标缓存,排查阶段先关,稳定后再按需开 pool-prepared-statements: false max-open-prepared-statements: 0 filters: stat,walloracle.jdbc.implicitStatementCacheSize这个参数是重点。它默认会缓存预编译语句,缓存本身不释放游标,高并发下很容易把open_cursors顶满。排查阶段先设 0,确认稳定后再逐步调大观察。
3.4 TaoToken config.toml 配置骨架
把模型调用和排查脚本统一到一个通道,配置集中管理。下面这份骨架可以直接改 Key 用。
# config.toml - TaoToken 统一 Key/API 通道配置骨架 [default] # API 基址,固定不加 UTM base_url = "https://taotoken.net/api" # Key 从环境变量读取,不要硬编码 api_key = "${TAOTOKEN_API_KEY}" # 请求超时,排查脚本建议设长一点 timeout_seconds = 60 # 失败重试次数 max_retries = 3 [models] # 默认对话模型 chat = "gpt-4o-mini" # 代码/排查脚本生成用 coding = "claude-3-5-sonnet" [logging] level = "info" # 排查阶段打开请求日志,方便定位 log_requests = trueKey 在控制台创建:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。环境变量设置:
export TAOTOKEN_API_KEY="你的Key"4. 验证请求:确认游标生效与通道连通
4.1 验证 open_cursors 是否生效
改完参数后,新开一个会话查一次,再跑一段会开游标的 SQL 观察计数。
-- 新会话确认上限 SELECT value FROM v$parameter WHERE name = 'open_cursors'; -- 观察当前会话游标数变化 SELECT COUNT(*) FROM v$open_cursor WHERE sid = SYS_CONTEXT('USERENV','SID');如果上限显示 1000,且业务高峰时单会话游标数稳定在 200 以内,说明调整到位。如果还是往上涨,回到 3.1 的会话统计继续找泄漏点。
4.2 验证 TaoToken 通道连通
用 curl 发一个最小请求,确认 Key 和基址都对。
curl -s -X POST "https://taotoken.net/api/v1/chat/completions" \ -H "Authorization: Bearer ${TAOTOKEN_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "ping"}], "max_tokens": 10 }'返回里带choices字段就说明通道正常。如果返回 401,检查 Key 是否复制完整;返回 404,检查base_url有没有多写或少写/v1。想直接在网页里试模型,可以用模型对话入口:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
4.3 把排查脚本接进通道
一个实用做法:把 3.1 的游标统计 SQL 结果丢给模型,让它帮你判断哪个会话异常。脚本里读config.toml的base_url和api_key,请求走同一个通道,不用再单独配密钥。
import os, tomllib, requests with open("config.toml", "rb") as f: cfg = tomllib.load(f) base = cfg["default"]["base_url"] key = os.environ["TAOTOKEN_API_KEY"] resp = requests.post( f"{base}/v1/chat/completions", headers={"Authorization": f"Bearer {key}"}, json={ "model": cfg["models"]["coding"], "messages": [{"role": "user", "content": "分析这段游标统计:..."}] }, timeout=cfg["default"]["timeout_seconds"], ) print(resp.json()["choices"][0]["message"]["content"])5. 本篇常见错排查
改了 open_cursors 但报错依旧:先确认改的是不是当前实例。RAC 环境下ALTER SYSTEM默认只影响当前节点,需要SID='*'或逐节点执行。另外老连接不会自动继承新参数,重启连接池或等连接自然淘汰。
ORA-01000 消失了但 ORA-00604 还在:说明递归 SQL 的触发源不是游标,可能是触发器里的 DDL、审计策略或权限问题。查v$sql里最近执行的递归语句,或者开 10046 trace 抓递归调用链。
连接池调小后吞吐下降:maximum-pool-size不是越大越好。Oracle 每连接有固定内存开销,连接数过多反而拖慢。先按CPU 核数 * 2 + 磁盘数估算,再压测微调。
Druid 的pool-prepared-statements关了性能变差:这是排查期的取舍。稳定后可以重新打开,但把max-open-prepared-statements设成单连接游标预算的 1/3 以内,别让它无限缓存。
TaoToken 请求偶发超时:检查timeout_seconds是否太短,长文本排查建议 60 秒以上;同时确认max_retries生效,网络抖动时能自动重试。
Key 泄漏风险:永远不要把 Key 写进config.toml提交到仓库,用${TAOTOKEN_API_KEY}占位,CI 里通过 secrets 注入。
6. 固化配置:把游标上限和 API 通道一起管起来
排查完别急着收工,把这次的动作固化成可复用的东西。数据库侧,把open_cursors的目标值写进初始化脚本,新环境部署时自动带上;应用侧,把连接池的游标缓存参数写进配置模板,避免下次有人手滑打开。
如果你还在做长期编码或 Agent 类项目,需要频繁调用模型,可以看下 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,它把常用编码模型的调用打包成一个通道,配合上面的config.toml骨架直接能用。接入细节和参数说明在文档里:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
最后留一个我踩过的坑:open_cursors调到 1000 后别就不管了,加个监控,单会话游标数超过阈值就告警。游标泄漏往往是代码里某个ResultSet忘了关,参数只是给你争取排查时间,真正止血还得靠代码 review 和连接池配置双管齐下。