简介:数据库死锁常被误判为性能瓶颈,实际表现为连接池耗尽、请求堆积与锁等待蔓延。本文结合一次真实排查过程,梳理死锁的识别路径、锁等待分析手段与处置方法,帮助读者建立从现象到根因的排查框架,并给出可落地的预防建议。
某订单系统在业务高峰期突然出现大量接口超时,应用层不断抛出无法获取数据库连接的异常。查看监控后,发现数据库活跃连接数从日常的 50 左右迅速攀升至 500 以上,已经触及连接池上限;与此同时,数据库 CPU 使用率却维持在 20% 左右的正常水平,并没有出现明显的计算瓶颈。
进一步观察,慢查询数量在短时间内明显增加,接口平均响应时间从 100 毫秒恶化到 30 秒以上。更棘手的是,第一次重启服务后系统只恢复了大约 10 分钟,随后又进入同样的故障状态。
这种“CPU 不高、连接数打满、请求大面积超时”的组合,通常指向一个常见但容易被误判的问题:大量会话并没有在执行复杂计算,而是阻塞在锁等待上,导致连接被长期占用、无法及时释放。
从监控数据可以得出几个关键结论:
因此初步判断方向为:部分 SQL 被长期阻塞,导致连接无法归还连接池,进而引发雪崩式超时。
首先通过数据库的会话列表查看所有连接正在执行的操作。简化后可以看到大量会话处于类似状态:
| 会话 ID | 命令 | 持续时间 | 状态 | SQL |
|---|---|---|---|---|
| 1001 | Query | 180s | Waiting for table lock | UPDATE orders SET status=2 WHERE id=123 |
| 1002 | Query | 178s | Waiting for table lock | UPDATE orders SET status=2 WHERE id=456 |
| 1003 | Query | 175s | Sending data | SELECT * FROM orders WHERE user_id=789 |
大量更新语句卡在“等待表锁”状态,且等待时间已经达到数百秒。这些会话并不会消耗大量 CPU,但会一直占用应用连接池中的连接。
此时需要区分一个关键概念:表锁等待并不等同于死锁。表锁等待只说明存在资源竞争,而死锁则是多个事务相互持有对方需要的锁,形成闭环等待。要确认是否发生死锁,必须进一步查看锁等待关系。
在 MySQL 5.7 及以上版本中,可以通过 performance_schema 查看 InnoDB 锁等待关系,也可以通过 information_schema 中的锁等待表进行分析。典型查询逻辑如下:
SELECTr.trx_id AS waiting_trx_id,r.trx_mysql_thread_id AS waiting_thread,r.trx_query AS waiting_query,b.trx_id AS blocking_trx_id,b.trx_mysql_thread_id AS blocking_thread,b.trx_query AS blocking_queryFROM information_schema.innodb_lock_waits wJOIN information_schema.innodb_trx bON b.trx_id = w.blocking_trx_idJOIN information_schema.innodb_trx rON r.trx_id = w.requesting_trx_id;
查询结果暴露出关键信息:
| 等待事务 | 等待 SQL | 阻塞事务 | 阻塞 SQL |
|---|---|---|---|
| 10001 | UPDATE orders SET status=2 … | 10002 | UPDATE inventory SET stock=… |
| 10002 | UPDATE inventory SET stock=… | 10001 | UPDATE orders SET status=2 … |
从结果可以清晰看到,事务 10001 和 10002 互相持有对方需要的锁资源,同时又在等待对方释放锁。此时已经不是单纯的锁排队,而是典型的死锁闭环。
该业务场景中,一个订单状态更新操作通常涉及多张表。事务 A 先更新订单表,再更新库存表;事务 B 先更新库存表,再更新订单表。当两个事务交错执行时,就可能出现:
双方都无法继续,只能等待数据库死锁检测机制介入。在一些参数配置下,如果死锁检测未能及时触发或者表锁等待时间设置过长,就会导致大量会话长期堆积。
死锁本身只涉及少量事务,但为什么会导致数百个连接被占用?原因在于连锁反应:
其中,应用层的重试机制在故障中起到了放大作用。没有设置合理的重试上限、退避策略或熔断机制时,重试会迅速将局部锁冲突放大为全系统不可用。
排查到死锁闭环后,首要操作不是重启数据库,而是终止阻塞源头事务。可以通过会话 ID 精确结束持有锁但不释放的事务:
KILL <blocking_thread_id>;
本次事故中,阻塞事务对应的线程 ID 明确后,只终止其中一个事务,死锁闭环即被打破,后续等待会话迅速完成,连接池水位在短时间内回落。
但需要注意,仅终止线程只能恢复当下故障,并不能防止复发。必须结合业务 SQL 的顺序与事务设计进行根治。
最有效的死锁预防手段是保证所有事务按照相同顺序访问资源。比如统一为:
即使无法完全避免锁竞争,也可以将死锁概率降到极低。
事务粒度越大,持锁时间越长,与其他事务冲突的概率越高。应避免在单个事务中执行过多更新、远程调用或不必要的查询。非关键操作可以后置到事务提交后执行。
合理设置锁等待超时时间,避免单个会话无限期阻塞。虽然超时会返回错误,但相比长时间占满连接池,更有利于保护整体可用性。
应用层应限制重试次数,采用指数退避与随机抖动,并引入熔断机制。当数据库连接获取失败率达到阈值时,快速失败比继续堆积请求更安全。
对以下指标配置告警,可以更早发现死锁风险:
提前预警有助于在故障扩大前介入处理。
这次故障表面上是一次连接池耗尽问题,实际根因是数据库死锁引发的请求堆积与重试放大。排查过程中,先通过会话状态判断阻塞方向,再通过锁等待视图确认闭环关系,最后通过终止源头事务恢复服务。
对于类似的线上事故,核心经验可以归纳为:不要只看资源水位,也要关注等待关系。CPU 正常、连接数打满、请求超时这三者同时出现时,锁等待与事务阻塞应被优先排查。只有建立从现象到锁等待链的完整分析路径,才能在复杂故障中快速定位根因,避免反复重启带来的不确定性。