慢 SQL 不一定是 SQL 慢:一次 MySQL 连接池耗尽的故障定位

接口变慢时,很多人第一反应是去翻慢 SQL。这没错,但有一个前提:用户等待的时间真的花在 SQL 执行上。

这次 service-japi-order-x 就很典型。入口 P99 从 220ms 升到 5.2s,Tomcat busyThreads 从 90 涨到 260,错误日志开始出现:

复制代码
SQLTransientConnectionException:
Connection is not available, request timed out after 30000ms

如果只盯慢日志,很容易得出"数据库慢了"的结论。但真正的问题不是 SQL 普遍变慢,而是应用侧拿不到数据库连接。

第一眼像慢 SQL,但证据不支持

故障窗口里,最先拉的是入口、JVM 和数据库资源指标。结果有点反直觉:

指标 故障前 故障中
api-order-x P99 220ms 5200ms
Tomcat busyThreads 90 260
应用 CPU 平稳 约 45%
GC 平稳 无明显异常
数据库 CPU/IO 平稳 未打满

这组证据说明两件事。

第一,应用不是 CPU 打满,也不是 GC 把请求停住了。第二,数据库也没有表现出整体资源耗尽。如果是典型慢 SQL 扩散,通常会看到慢日志增加、数据库 CPU 或 IO 同步上升,或者执行计划明显退化。

但这里异常日志指向了另一个方向:请求不是"执行 SQL 慢",而是在 getConnection() 阶段等连接。

真正的异常在连接池等待

继续看 Hikari 指标,问题就清楚了:

复制代码
HikariPool-1 maximumPoolSize=80
active=80
idle=0
pending=360
connectionTimeout=30000ms
getConnection timeout count 持续增长

active=80 说明连接池已经打满,idle=0 说明没有空闲连接可借,pending=360 说明大量请求正在排队等连接。这个状态下,业务线程会被阻塞在获取连接上,最终拖高 Tomcat busyThreads。

所以入口耗时的主要组成不是 SQL 执行耗时,而是连接池等待时间。慢 SQL 只是候选原因之一,它可能导致连接长时间不归还,但不能直接等同于"SQL 本身慢"。

这个阶段最关键的判断是:连接为什么不归还?

常见方向有四个:

方向 证据入口 本次结论
连接泄漏 active 是否长期不回落、异常路径是否关闭连接 不像典型泄漏
慢 SQL 慢日志、执行计划、单次 SQL 耗时 普通 select 未明显变慢
锁等待 processlist、data_lock_waits 命中
事务过长 innodb_trx、事务持续时间 命中

连接泄漏的典型表现是故障解除后 active 仍长期不回落,或者连接数随时间单向增长。本次故障恢复后 active 能回落,所以它不是主线。

连接被谁占住了

数据库侧证据把问题收敛到了锁等待和长事务:

复制代码
information_schema.innodb_trx 显示 1 个事务持续 420s
performance_schema.data_lock_waits 中 table_order_x 存在锁等待
https://zhida.zhihu.com/search?content_id=278345624&content_type=Article&match_order=1&q=SHOW+PROCESSLIST&zhida_source=entity 中多条 update table_order_x 处于 Waiting for row lock
慢日志中普通 select 耗时未明显升高

这组数据很有决定性。

innodb_trx 里有一个持续 420s 的事务,说明某个连接长时间持有事务上下文。data_lock_waitsSHOW PROCESSLIST 又显示 table_order_x 上存在行锁等待,多条 update 卡在 Waiting for row lock

这就形成了闭环:

  1. 长事务持有行锁;
  2. 同表更新请求进入锁等待;
  3. 等待中的 SQL 持续占用数据库连接;
  4. 应用连接池 active 被占满;
  5. 新请求在 getConnection() 等待,直到 30s 超时;
  6. Tomcat 业务线程被拖住,接口 P99 和超时率上升。

所以这不是"SQL 语句突然慢了",而是"事务边界和锁等待把连接池耗尽了"。SQL 只是被堵在锁上,连接池才是用户侧延迟放大的关键资源。

止血和修复不能只调大连接池

这类问题最容易做错的动作,是把 maximumPoolSize 从 80 调到更大。短时间看,pending 可能下降;但如果锁等待还在,更多连接只会把更多请求送进数据库等待队列,扩大数据库端压力。

更合理的处理顺序是:

动作 目的
终止异常长事务或暂停相关入口 先释放被持有的行锁
对 api-order-x 做临时限流 防止等待连接的请求继续堆积
检查事务内逻辑 排除事务内 RPC、复杂计算、批量循环更新
收敛更新范围和顺序 降低行锁冲突概率
为连接池等待建立告警 让问题在接口超时前暴露

修复后验证不能只看接口恢复,还要同时看连接池和数据库锁状态:

复制代码
Hikari active 从 80 回落到稳定水位
pending 从 360 回落到 0
getConnection timeout 不再增长
SHOW PROCESSLIST 中 Waiting for row lock 消失
api-order-x P99 回到 220ms 附近

这些指标一起恢复,才说明"连接池耗尽"这个故障链路被切断。

可复用排查命令

排查 MySQL 连接池耗尽时,我一般按"应用连接池 -> 线程阻塞点 -> 数据库连接和锁 -> 事务边界"的顺序看。

复制代码
curl -s http://service-j/actuator/metrics/hikaricp.connections.active

看 active 是否接近 maximumPoolSize,以及是否长期不回落。

复制代码
curl -s http://service-j/actuator/metrics/hikaricp.connections.pending

看是否有大量线程等待连接。pending 上升通常比接口超时更早暴露问题。

复制代码
jstack <pid> > /tmp/thread.dump
grep -n "getConnection\\|HikariPool" /tmp/thread.dump

确认业务线程是否阻塞在获取数据库连接,而不是阻塞在本地锁、RPC 或 Redis。

复制代码
SHOW FULL PROCESSLIST;

看数据库端连接正在执行什么,是否大量处于 Waiting for row lockSending data 或长时间 Sleep。

复制代码
SELECT * FROM information_schema.innodb_trx\G

看是否存在持续时间异常的事务,重点关注事务开始时间、持锁状态和对应线程。

复制代码
SELECT * FROM performance_schema.data_lock_waits\G
SELECT * FROM performance_schema.data_locks\G

定位阻塞事务和被阻塞事务,确认锁等待发生在哪张表、哪个索引或哪类记录上。

复制代码
SHOW ENGINE INNODB STATUS\G

用于补充锁等待、死锁和事务状态细节,适合在锁等待视图信息不足时交叉验证。

复盘要点

  • 直接原因:table_order_x 上存在锁等待,部分更新语句长时间占用连接,导致 Hikari 连接池 active 打满。
  • 根因分析:异常长事务扩大了锁持有时间,连接池等待时间被入口请求感知,最终表现为接口 P99 和超时率上升。
  • 容易误判:接口慢不等于 SQL 慢,慢日志不明显时要检查连接池等待、锁等待和事务持续时间。
  • 预防措施:连接池 active、pending、timeout 必须纳入核心告警,并和接口 P99、数据库锁等待放在同一张排障看板。
  • 长期治理:控制事务边界,避免事务内调用外部依赖或做大批量循环更新;稳定性建设不能依赖无限扩资源,而要让关键资源可观测、可定位、可恢复。
相关推荐
海兰6 小时前
【数据库】tdsql(mysql8.0)优化思考三
数据库·redis
煎饼学大模型7 小时前
Agent 的“大脑-手“解耦架构:当推理层和工具执行层各自独立演进
数据库·人工智能·oracle·架构·agent
Lucifer三思而后行8 小时前
Veeam 备份 Oracle RAC 失败,是归档删除脚本的锅!
sql
暖和_白开水10 小时前
数据分析agent(十三_3):docker mysql容器镜像服务的异常错误
mysql·docker·容器
muddjsv11 小时前
SQLite数据库文件机制详解:journal、wal、shm文件作用与备份避坑
数据库·sqlite
吴声子夜歌12 小时前
MongoDB 4.2——应用程序设计
数据库·mongodb
数字新视界12 小时前
动环监控可视化在数据中心管理中的应用与实践解析
数据库·物联网·解决方案·动环监控·大榕树
极地野狼 音乐哔哔12 小时前
数据库备份恢复的原理实践
数据库
互联网中的一颗神经元12 小时前
小白python入门 - 25. SQL 与表设计入门
数据库·python·sql
ttwuai12 小时前
AI 生成后台改数据后,操作日志别只记按钮:Go + MySQL 怎么验
数据库·人工智能·mysql·golang