一次生产事故复盘:服务报 “Failed to obtain JDBC Connection” 的完整排查与解决


一、问题现象

系统运行一段时间后,开始频繁出现如下异常:

Failed to obtain JDBC Connection; 
nested exception is java.sql.SQLTransientConnectionException: 
swxx - Connection is not available, request timed out after 30000ms.

表现为:

  • 接口全部超时
  • 定时任务报错
  • 数据库连接获取失败
  • 服务响应变慢
  • 服务器 CPU 飙升接近 100%

重启服务后短暂恢复,运行一段时间再次复现。


二、第一反应(错误判断)

看到这个异常,第一直觉是:

是不是数据库连接泄漏了?

于是我做了这些操作:

1️⃣ 开启连接泄漏检测

remove-abandoned: true
remove-abandoned-timeout: 180
log-abandoned: true

2️⃣ 开启连接保活

test-while-idle: true
validation-query: SELECT 1
time-between-eviction-runs-millis: 60000

结果:

❌ 问题依旧存在
❌ 没有明显泄漏日志


三、进一步排查

1️⃣ 查看数据库连接数

show global status like 'Threads_connected';

发现:

  • 服务启动前:241
  • 启动后:271

只增加了 30 个连接。

说明:

不是连接无限增长型泄漏


2️⃣ 查看正在执行的 SQL

show processlist;

发现大量相同 SQL:

  • 状态:Sending data
  • 执行时间:2000+ 秒
  • 同一条 SQL 被并发执行

而且:

最长执行时间超过 40 分钟还未结束。

问题开始清晰。


四、真正原因

核心问题不是连接泄漏。

而是:

慢 SQL 长时间占用数据库连接,导致连接池被耗尽。

问题 SQL 结构(核心片段)

and not (
    dpg <= CASE ...
    AND (
        SELECT SUM(t2.drp)
        FROM wxddz_r_202602 t2
        WHERE t2.adz = t1.adz
          AND t2.ecd = t1.ecd
          AND t2.tm > DATE_SUB(t1.tm, INTERVAL 24 HOUR)
          AND t2.tm <= t1.tm
    ) <= CASE ...
)

这是一个:

相关子查询(Correlated Subquery)

含义是:

  • 外层扫描一行
  • 内层子查询再执行一次

如果外层扫描 10 万行:

👉 内层子查询就执行 10 万次
👉 每次还可能扫描大量数据

这是指数级性能灾难。


五、连锁反应机制

整个问题的完整链路是:

1️⃣ SQL 执行时间极长(40分钟)
2️⃣ 连接一直被占用
3️⃣ 连接池 maxActive 很小(如 6)
4️⃣ 新请求无法获取连接
5️⃣ 30秒后超时
6️⃣ 报 Failed to obtain JDBC Connection
7️⃣ 由于数据库疯狂扫表
8️⃣ CPU 被打满

这不是连接不释放。

而是:

连接被“合法地”长时间占用。


六、为什么 CPU 会爆满?

在这里插入图片描述

MySQL 状态显示:

Sending data

这通常意味着:

  • 正在做大量数据扫描
  • 可能 using temporary
  • 可能 using filesort
  • 没有命中索引

如果是全表扫描 + 子查询循环扫描:

数据库 CPU 会持续 100%。


七、根本优化方案

方案一:加复合索引(必要)

如果没有如下索引:

ALTER TABLE wxddz_r_202602 
ADD INDEX idx_adz_ecd_tm (adz, ecd, tm);

子查询必定全表扫。

这是第一步。


方案二:避免相关子查询

相关子查询是性能杀手。

优化思路:

  • 先汇总 24 小时数据
  • 再 JOIN
  • 或改为分批处理
  • 或程序层面计算

在 MySQL5.7 没有窗口函数的情况下:

可以改为临时汇总表。


方案三:分批处理

原 SQL:

WHERE t1.id > 9062999 AND t1.id < 9076452

如果数据量大:

建议:

  • 分批 1000 条
  • 逐批计算
  • 控制并发

不要一次性大范围扫描。


方案四:限制定时任务并发

如果是 Quartz 任务:

@DisallowConcurrentExecution

避免同一任务并发执行。

否则慢任务未完成,新任务又启动。


八、数据库参数是否有帮助?

比如:

max_connections=400

是否有用?

答案:

❌ 没本质帮助

如果慢 SQL 不解决:

  • 400 个连接 = 400 个慢查询
  • 数据库更快崩溃

根因不在连接数。


九、事故总结

这次问题本质是:

规则计算型 SQL 被写成在线实时查询
且使用相关子查询导致指数级性能问题

最终表现为:

  • 连接池耗尽
  • JDBC 获取失败
  • 服务不可用
  • CPU 爆满

十、经验总结(非常重要)

1️⃣ 出现 JDBC Connection is not available

不要第一时间怀疑连接泄漏。

先做:

show processlist;

看是否有长时间 SQL。


2️⃣ 相关子查询慎用

结构像:

select ...
where exists (
   select ...
)

在大表中极易出问题。


3️⃣ 定时任务必须控制并发

慢任务叠加是灾难。


4️⃣ 数据规则计算应尽量:

  • 批处理
  • 预汇总
  • 离线计算

不要在线复杂聚合。


最终结论

这次问题不是连接池问题。
不是 max_connections 问题。
不是连接泄漏问题。

而是:

一条极慢 SQL 长时间占用连接,导致连接池耗尽。

当你看到:

Failed to obtain JDBC Connection

请优先排查:

是否存在长时间执行的慢 SQL。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐