拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ORA-01000: maximum open cursors exceeded 排查笔记:Java 应用连接 Oracle 的游标泄漏定位与 TaoToken 配置骨架

ORA-01000: maximum open cursors exceeded 排查笔记:Java 应用连接 Oracle 的游标泄漏定位与 TaoToken 配置骨架 1. 先别急着调大 open_cursorsORA-01000 到底在说什么ORA-01000: maximum open cursors exceeded是 Java 应用连 Oracle 时非常典型的一类运行期报错。它的字面意思是当前会话session已经打开的游标数量超过了数据库参数open_cursors允许的上限。注意这里的关键词是「会话级」——不是整库、不是整机而是某一个连接上堆积了太多没释放的游标。很多同学第一次遇到它第一反应是让 DBA 把open_cursors从 300 调到 3000然后重启应用世界安静了两天第三天又炸了。原因很简单游标泄漏是代码或连接池配置的问题调大参数只是把「爆炸时间」往后推泄漏速度不变迟早还会撞上限。这篇笔记就按「复现 → 定位 → 修复 → 验证」的闭环来写聚焦三个最常见的泄漏源连接池配置、未关闭的 Statement/ResultSet、批量操作里的游标累积。同时给出一套用 TaoToken 统一 Key/API 通道做接入骨架的配置方式方便你在排查过程中顺手把模型调用通道也规整好。适合谁看正在维护 Java Oracle 老系统、被这个报错反复骚扰的后端同学以及想搞清楚「游标到底什么时候开、什么时候关」的初中级开发者。全文的 SQL 和 Java 片段都可以直接复制去用。2. 前置准备确认版本、拿到可观测的入口在动手改代码之前先把「现场」固定下来否则你改完也不知道是不是真的修好了。2.1 确认 Oracle 侧的两个参数连上出问题的那个 schema执行下面这段查询看清楚当前上限和已经用掉的量-- 当前会话的游标上限 SELECT name, value FROM v$parameter WHERE name open_cursors; -- 每个会话当前打开的游标数按数量倒序 SELECT s.sid, s.serial#, s.username, s.machine, s.program, COUNT(*) AS cursor_count FROM v$open_cursor oc JOIN v$session s ON s.sid oc.sid GROUP BY s.sid, s.serial#, s.username, s.machine, s.program ORDER BY cursor_count DESC;第一段告诉你「天花板有多高」第二段告诉你「谁快顶到天花板了」。如果某个program对应的会话游标数长期在几百以上基本可以锁定是应用侧没关干净。注意v$open_cursor需要有一定权限才能查普通业务账号可能看不到找 DBA 开一下只读权限即可不要用业务账号去改参数。2.2 确认 JDBC 驱动与连接池版本游标行为和驱动、连接池都有关。Oracle JDBC 驱动在implicit statement caching上有默认行为某些版本默认缓存 10 个语句连接池HikariCP、Druid、DBCP又各自有 statement 缓存开关。先把版本记下来!-- pom.xml 里确认这两行 -- dependency groupIdcom.oracle.database.jdbc/groupId artifactIdojdbc8/artifactId version21.9.0.0/version /dependency dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version5.1.0/version /dependency版本本身不是罪魁祸首但它决定了你后面该关哪个开关。比如 HikariCP 的cachePrepStmts如果和 Oracle 驱动自带的缓存叠加容易出现「你以为关了、其实还缓存着」的错觉。2.3 用 TaoToken 把排查期的模型调用通道固定下来排查这种问题经常需要一边翻日志、一边让模型帮你解释堆栈或生成校验 SQL。如果每个工具各配一套 Key很容易乱。我习惯用 TaoToken 做统一入口官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 。它的作用是给你一个统一的 Key 和 API 通道模型对话、编码辅助都走同一个出口排查时不用来回切配置。在项目根目录建一个settings.json把通道骨架先搭好{ provider: taotoken, api_base: https://taotoken.net/api, api_key: sk-你的TaoTokenKey, models: { chat: claude-sonnet, code: claude-sonnet }, timeout_ms: 60000, retry: { max_attempts: 3, backoff_ms: 800 } }Key 的申请入口在控制台的 API Keys 页面https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。这一步只是把通道准备好真正排查游标还是靠 SQL 和代码别本末倒置。3. 可复制配置三个泄漏源逐个堵3.1 连接池配置别让 statement 缓存变成隐形游标先看一段典型的 HikariCP 配置问题往往出在「缓存开着但没人管」spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 # 关键Oracle 场景下建议显式关闭驱动侧隐式语句缓存 >spring: datasource: druid: pool-prepared-statements: false max-open-prepared-statements: 0 filters: stat,wallpool-prepared-statements: false是保守但安全的起点。等确认没有泄漏后再按需开启并配合监控。3.2 代码侧Statement 和 ResultSet 必须成对关闭最经典的泄漏写法长这样// 反例循环里创建 Statement且没有关闭 public void badQuery(ListLong ids) throws SQLException { Connection conn dataSource.getConnection(); for (Long id : ids) { Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(SELECT * FROM orders WHERE id id); while (rs.next()) { // 处理数据 } // 这里既没关 rs也没关 stmt } conn.close(); }每循环一次数据库侧就多一个打开的游标循环一万次就是一万个。正确写法用 try-with-resources让 JVM 保证关闭public void goodQuery(ListLong ids) throws SQLException { String sql SELECT * FROM orders WHERE id ?; try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (Long id : ids) { ps.setLong(1, id); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { // 处理数据 } } } } }注意这里PreparedStatement提到了循环外面循环里只重设参数、只关ResultSet。这是把「游标打开次数」从 N 次降到 1 次的关键改动。3.3 批量操作addBatch 之后别忘了 clearBatch批量插入/更新是另一个高发区。看这段// 反例batch 一直加从不 clear也不分段提交 PreparedStatement ps conn.prepareStatement(INSERT INTO logs(msg) VALUES(?)); for (LogItem item : items) { ps.setString(1, item.getMsg()); ps.addBatch(); } ps.executeBatch();当items很大时batch 内部会累积大量待执行语句某些驱动实现下会对应大量游标。稳妥做法是分段int batchSize 500; int count 0; try (PreparedStatement ps conn.prepareStatement(INSERT INTO logs(msg) VALUES(?))) { for (LogItem item : items) { ps.setString(1, item.getMsg()); ps.addBatch(); if (count % batchSize 0) { ps.executeBatch(); ps.clearBatch(); } } ps.executeBatch(); ps.clearBatch(); }clearBatch()是很多人会漏的一步。它把已执行的批次清掉避免游标和内存双重累积。3.4 用 TaoToken 的 coding-plan 通道辅助生成校验脚本排查过程中经常要临时写校验 SQL 或解析日志。可以在 TaoToken 的 coding-plan 页面把编码辅助通道配好https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。它和前面的settings.json是同一套 Key不用重复配置。需要临时对话验证模型是否正常时用模型对话入口https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。4. 验证请求从复现到确认修复改完之后不能靠「感觉好了」要有可观测的证据。4.1 复现阶段先记录基线在修复前跑一次会触发问题的压测或批处理同时每隔 5 秒采样一次游标数-- 采样脚本观察目标会话游标增长曲线 SELECT oc.sid, COUNT(*) AS cursor_count, TO_CHAR(SYSDATE, HH24:MI:SS) AS sample_time FROM v$open_cursor oc WHERE oc.sid ( SELECT sid FROM v$session WHERE program LIKE %你的应用名% AND ROWNUM 1 ) GROUP BY oc.sid;如果修复前曲线是持续上升、修复后是锯齿状升上去又降回来说明关闭逻辑生效了。4.2 用 JDBC 侧监控确认 Statement 关闭在连接池上挂一个 Statement 监听统计打开和关闭次数Slf4j public class CursorMonitor implements StatementEventListener { private final AtomicLong opened new AtomicLong(); private final AtomicLong closed new AtomicLong(); Override public void statementClosed(StatementEvent event) { closed.incrementAndGet(); } Override public void statementErrorOccurred(StatementEvent event) { log.warn(statement error: {}, event.getSQLException().getMessage()); } public void logDiff() { log.info(opened{}, closed{}, diff{}, opened.get(), closed.get(), opened.get() - closed.get()); } }把opened - closed的差值打到日志里稳定运行一段时间后这个差值应该趋近于 0。如果一直为正且持续增长说明还有地方没关。4.3 用 TaoToken 通道做一次连通性验证配置好settings.json后用 curl 验证通道是否通curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的TaoTokenKey \ -H Content-Type: application/json \ -d { model: claude-sonnet, messages: [{role: user, content: 用一句话解释 Oracle 游标是什么}] }返回正常 JSON 就说明 Key 和通道没问题。这一步和游标修复是两条线但排查时经常需要模型帮忙读堆栈通道稳定能省不少事。接入细节可以对照文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。5. 本篇常见错排查5.1 改了代码还是报 ORA-01000先确认改的代码真的生效了——老系统经常有多个数据源、多个DataSourceBean你改的可能是没被用到的那一个。用v$open_cursor里的program和machine字段反查是哪个进程在泄漏再对应到具体模块。5.2 连接池关了缓存性能反而下降implicitStatementCacheSize: 0会让每次prepareStatement都真实解析一次 SQL。如果 QPS 很高可以改成保留缓存但配合clearBatch和正确的关闭逻辑。缓存不是原罪泄漏才是。先修泄漏再谈性能。5.3 批量操作分段后仍然增长检查executeBatch()之后有没有clearBatch()以及ResultSet是否在executeQuery后立即关闭。有些 ORM 框架如 MyBatis 的ExecutorType.BATCH会自己管理 batch这时要确认框架版本是否有已知的游标泄漏 issue。5.4 TaoToken 返回 401先确认settings.json里的api_key没有多余空格再确认请求头是Authorization: Bearer sk-xxx。如果 Key 是在控制台刚生成的注意有些环境需要几秒钟生效。还不行就去 API Keys 页面重新生成一个https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。5.5 采样 SQL 查不到目标会话v$open_cursor只显示当前打开的游标如果会话刚好在两次采样之间关闭了游标可能采不到。把采样频率提高到 1 秒或者改用v$sesstat里的opened cursors cumulative统计累计值看增长趋势更稳。6. 把通道和排查流程一起固化下来游标泄漏的修复闭环其实就三步用v$open_cursor找到泄漏会话用 try-with-resources 和clearBatch堵住代码漏洞用采样曲线确认差值归零。连接池配置是辅助open_cursors调大是最后手段而不是第一手段。排查过程中如果需要一个稳定的模型调用通道来读日志、生成校验 SQLTaoToken 的统一 Key 和 API 通道可以省掉反复切配置的麻烦。接入骨架就是前面那份settings.json验证用 curl 打一次/api/v1/chat/completions即可。需要长期做编码辅助或 Agent 类任务的话Coding Plan 入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 和 API Keys 共用同一套凭证。把这两条线都固定下来下次再遇到 ORA-01000你至少知道从哪里开始查。
返回列表