Druid 连接池配置踩坑记:YashanDB 报 YAS-04003 打开游标数过多怎么排查
1. 从一次线上告警说起YAS-04003 到底在说什么YAS-04003 这个报错字面意思是「打开游标数过多」完整提示通常是YAS-04003: maximum number of open cursors is 1000。它不是一个连接数问题也不是内存问题而是数据库层面在告诉你当前这个会话或者整个实例里还没被关闭的游标数量已经顶到了OPEN_CURSORS参数设定的上限。游标这个概念你可以把它理解成数据库执行一条 SQL 时开的一个「临时窗口」。每执行一次查询或更新数据库内部就会打开一个游标来承载结果集或执行上下文语句执行完、结果集被消费完、Statement 被关闭之后这个游标才会释放。如果应用层一直不关或者连接池帮你「缓存」着不关游标就会越积越多直到撞上 1000 这个默认天花板。这个报错最容易出现在 Java 应用 连接池 国产数据库的组合里尤其是 Druid 搭配 YashanDB 的场景。原因很典型Druid 有一套 PreparedStatement 缓存机制本意是减少 SQL 预编译开销、提升性能但它和 YashanDB 对游标的计数方式一旦没对齐就会把「缓存」变成「泄漏」。我见过不少团队第一反应是去调大OPEN_CURSORS结果只是把爆炸时间往后推根因还在。这篇就按排查路径来先确认现象和影响面再讲清楚 Druid 侧哪些参数在作祟然后给出可复制的配置片段、YashanDB 侧的游标查询语句、复现与验证步骤最后把常见误判点列出来。适合正在用 Druid YashanDB、被这个报错卡住的后端和 DBA 同学跟做。2. 前置准备确认版本、拿到连接信息与 TaoToken 接入在动手改配置之前先把环境信息固定下来不然排查过程会来回摇摆。你需要确认三件事Druid 的版本、YashanDB 的版本、以及当前OPEN_CURSORS的实际取值。Druid 不同版本对share-prepared-statements和pool-prepared-statements的处理细节有差异YashanDB 不同版本对游标的统计口径也可能不同。如果你在本地或测试环境复现建议直接用一套独立的连接配置别在生产上试。连接信息host、port、service name、账号密码从你的配置中心或 DBA 那里拿。YashanDB 的 JDBC 驱动 jar 要放对位置驱动类名和 URL 格式按官方文档来别照搬 MySQL 的写法。这里顺带说一个提效的点排查这类问题时经常需要临时跑一些诊断 SQL、对比不同参数下的行为或者让 AI 帮你读一段 Druid 配置和报错日志。我平时会用 TaoToken 把模型对话和 API 调用统一起来省得在多个平台之间切。它的模型对话入口在https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel_chat接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdocAPI Key 在https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi_keys管理。API 基址是https://taotoken.net/api注意这个地址不带 UTM 参数配置客户端时直接用这个。注意TaoToken 在这里的角色是帮你做诊断辅助和日志分析不是替代你的数据库客户端也不是让你把生产库连接信息丢进去。诊断 SQL 还是在你自己的 YashanDB 会话里跑。3. 可复制配置Druid 侧到底哪几个参数在制造游标先把「肇事参数」摆出来。Druid 里和 PreparedStatement 缓存相关的核心配置有三个参数作用危险点share-prepared-statements是否在连接之间共享缓存的 PreparedStatement开启后同一 SQL 的 Statement 会被复用并长期持有pool-prepared-statements是否开启 PreparedStatement 池化开启后 Statement 不随连接关闭而释放max-open-prepared-statements每个连接最多缓存多少个 PreparedStatement设成正数会限制缓存量设成 -1 表示不限制问题就出在这三个参数同时开启、且max-open-prepared-statements设了一个正数比如 100的时候。Druid 会把每个连接上执行过的 PreparedStatement 缓存起来YashanDB 则把每一个未关闭的 Statement 都算作一个活动游标。连接池里有 N 个连接每个连接缓存 M 个 Statement游标数就是 N×M 量级很容易冲到 1000。下面是一份「先止血」的配置片段把缓存关掉让游标随语句执行完就释放spring: datasource: druid: url: jdbc:yashandb://127.0.0.1:1688/yashan username: app_user password: your_password driver-class-name: com.yashandb.jdbc.Driver initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 # 关键三行关闭 PreparedStatement 缓存 share-prepared-statements: false pool-prepared-statements: false max-open-prepared-statements: -1 validation-query: SELECT 1 FROM DUAL test-while-idle: true test-on-borrow: false test-on-return: false keep-alive: true如果你用的是druid.properties或纯 Java 配置对应写法是druid.share-prepared-statementsfalse druid.pool-prepared-statementsfalse druid.max-open-prepared-statements-1DruidDataSource ds new DruidDataSource(); ds.setUrl(jdbc:yashandb://127.0.0.1:1688/yashan); ds.setUsername(app_user); ds.setPassword(your_password); ds.setSharePreparedStatements(false); ds.setPoolPreparedStatements(false); ds.setMaxOpenPreparedStatements(-1);改完这三行重启应用观察一段时间。如果 YAS-04003 不再出现基本可以确认根因就在缓存策略上。但先别急着收工还要去 YashanDB 侧确认游标确实降下来了而不是被别的因素掩盖。4. 验证请求在 YashanDB 侧查游标、复现与确认修复配置改完只是第一步真正要看到的是数据库侧的游标数量变化。YashanDB 提供了几个视图可以查当前打开的游标。最直接的一条是SELECT b.sql_text FROM v$open_cursor a, v$sql b WHERE a.sql_id b.sql_id;这条语句会把当前所有打开游标对应的 SQL 文本列出来。如果同一段 SQL 反复出现很多次说明这些 Statement 没有被释放缓存或泄漏就在这条 SQL 上。你可以再加一层聚合看哪类 SQL 占用的游标最多SELECT b.sql_text, COUNT(*) AS cursor_cnt FROM v$open_cursor a, v$sql b WHERE a.sql_id b.sql_id GROUP BY b.sql_text ORDER BY cursor_cnt DESC;想定位到具体是哪个会话在堆积游标把v$session关联进来SELECT s.sid, s.serial#, s.username, s.program, COUNT(*) AS cursor_cnt FROM v$open_cursor a JOIN v$session s ON a.sid s.sid GROUP BY s.sid, s.serial#, s.username, s.program ORDER BY cursor_cnt DESC;复现步骤可以这样设计在测试环境把 Druid 的三个缓存参数按「肇事配置」打开max-open-prepared-statements设成 100然后用压测工具或循环脚本对同一个接口打几百次请求每次请求执行同一条带参数的查询。跑几分钟后去查上面的游标聚合语句你会看到那条 SQL 的cursor_cnt持续上涨。接着把配置改成关闭缓存重启再压同样的量游标数会稳定在一个低位不再单调增长。修复确认的标准不是「报错消失」而是「游标数在压测期间保持平稳」。如果只是报错消失但游标数还在缓慢爬升说明还有别的 Statement 没关比如代码里手动创建的Statement或ResultSet没有在finally里关闭。5. 本篇常见错排查别把锅全甩给连接池排查 YAS-04003 时有几个高频误判点我按踩坑顺序列一下。第一个坑是只调大OPEN_CURSORS。把 1000 改成 5000报错确实会晚点出现但游标泄漏的本质没解决只是把定时炸弹的引信拉长了。而且OPEN_CURSORS调太大会占用更多会话内存得不偿失。正确顺序是先查游标来源再决定要不要动这个参数。第二个坑是以为关了pool-prepared-statements就万事大吉。如果代码里用了 MyBatis 或 JPA框架自己也可能维护 Statement 缓存或者在某些写法下不释放ResultSet。这时候要结合v$open_cursor的结果去看到底是连接池缓存的还是业务代码泄漏的。区分方法连接池缓存通常表现为同一 SQL 在多个连接上各有一份代码泄漏通常表现为某个会话的游标数异常高。第三个坑是忽略share-prepared-statements和pool-prepared-statements的联动。这两个参数不是独立的share-prepared-statements为 true 时Druid 会在连接之间共享 Statement行为更复杂。排查阶段建议两个都关确认问题消失后如果确实需要性能优化再单独评估是否开启其中一个并配合max-open-prepared-statements设一个合理上限。第四个坑是没看连接池的实际连接数。max-active设成 50每个连接缓存 20 个 Statement理论上限就是 1000刚好撞线。所以排查时要把max-active和缓存参数一起看算一下最坏情况下的游标上限。第五个坑是诊断 SQL 本身写错。v$open_cursor和v$sql的关联字段在不同版本里可能有差异如果查出来是空结果先确认视图名和字段名别急着下结论说「没有游标泄漏」。如果你在分析这些视图输出或者读 Druid 日志时想让 AI 帮忙梳理可以用 TaoToken 的模型对话把日志片段贴进去问入口还是https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel_chat。长期做编码和 Agent 类任务的可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding_plan。6. 接入与排障入口把诊断流程固化下来这套排查流程跑通之后建议把它固化成团队的操作手册出现 YAS-04003 时第一步查v$open_cursor聚合第二步核对 Druid 三个缓存参数第三步算max-active × max-open-prepared-statements的理论上限第四步再决定是改配置还是改代码。需要查 API Key 和接入方式的走https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi_keys接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc控制台在https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsole。API 基址统一用https://taotoken.net/api不带 UTM。最后补一句实操经验改完 Druid 配置后别只看应用日志里报错没了就收工一定要在压测期间盯着v$open_cursor的计数曲线。曲线平稳才算真的修好曲线还在涨就回到第 5 节逐个排除。