接口变慢时很多人第一反应是去翻慢 SQL。这没错但有一个前提用户等待的时间真的花在 SQL 执行上。这次service-j的api-order-x就很典型。入口 P99 从 220ms 升到 5.2sTomcat busyThreads 从 90 涨到 260错误日志开始出现SQLTransientConnectionException: Connection is not available, request timed out after 30000ms如果只盯慢日志很容易得出“数据库慢了”的结论。但真正的问题不是 SQL 普遍变慢而是应用侧拿不到数据库连接。第一眼像慢 SQL但证据不支持故障窗口里最先拉的是入口、JVM 和数据库资源指标。结果有点反直觉指标故障前故障中api-order-x P99220ms5200msTomcat busyThreads90260应用 CPU平稳约 45%GC平稳无明显异常数据库 CPU/IO平稳未打满这组证据说明两件事。第一应用不是 CPU 打满也不是 GC 把请求停住了。第二数据库也没有表现出整体资源耗尽。如果是典型慢 SQL 扩散通常会看到慢日志增加、数据库 CPU 或 IO 同步上升或者执行计划明显退化。但这里异常日志指向了另一个方向请求不是“执行 SQL 慢”而是在getConnection()阶段等连接。真正的异常在连接池等待继续看 Hikari 指标问题就清楚了HikariPool-1 maximumPoolSize80 active80 idle0 pending360 connectionTimeout30000ms getConnection timeout count 持续增长active80说明连接池已经打满idle0说明没有空闲连接可借pending360说明大量请求正在排队等连接。这个状态下业务线程会被阻塞在获取连接上最终拖高 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 存在锁等待 SHOW PROCESSLIST 中多条 update table_order_x 处于 Waiting for row lock 慢日志中普通 select 耗时未明显升高这组数据很有决定性。innodb_trx里有一个持续 420s 的事务说明某个连接长时间持有事务上下文。data_lock_waits和SHOW PROCESSLIST又显示table_order_x上存在行锁等待多条update卡在Waiting for row lock。这就形成了闭环长事务持有行锁同表更新请求进入锁等待等待中的 SQL 持续占用数据库连接应用连接池 active 被占满新请求在getConnection()等待直到 30s 超时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 lock、Sending 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、数据库锁等待放在同一张排障看板。长期治理控制事务边界避免事务内调用外部依赖或做大批量循环更新稳定性建设不能依赖无限扩资源而要让关键资源可观测、可定位、可恢复。