死锁的本质:形成条件、检测机制与排查策略
文章目录
- 《死锁的本质:形成条件、检测机制与排查策略》
- 一、前言:什么是死锁
- 二、死锁的形成条件
- 三、死锁示例:经典案例
- 示例一:互相更新不同记录
- 示例二:范围更新导致间隙锁死锁
- 四、InnoDB 死锁检测机制
- 五、死锁日志分析
- 六、避免死锁的最佳实践
- ① 保持统一的加锁顺序
- ② 减少锁定范围
- ③ 控制事务粒度与时长
- ④ 合理选择事务隔离级别
- ⑤ 加锁顺序与索引命中调试
- 七、面试高频问题与答题模板
- 八、总结
《死锁的本质:形成条件、检测机制与排查策略》
一、前言:什么是死锁
大家好,我是程序员卷卷狗。
在数据库并发控制中,锁是保护一致性的必要手段。
但当多个事务互相等待对方释放锁时,
就会进入一种永远无法继续执行的状态——这就是死锁(Deadlock)。
一句话概括:
死锁是“事务之间的相互等待”,没有任何一方能先释放资源。
二、死锁的形成条件
系统要发生死锁,必须同时满足以下四个条件
| 条件 | 含义 | 示例 |
|---|---|---|
| ① 互斥条件 | 资源一次只能被一个事务占用 | 行锁、排他锁 |
| ② 占有且等待 | 持有部分锁的同时申请新锁 | 事务 A 锁 id=1,再锁 id=2 |
| ③ 不可剥夺 | 已获得的锁不能被强制释放 | InnoDB 不会强行回滚其他事务 |
| ④ 循环等待 | 两个事务形成等待环 | A 等 B,B 等 A |
只要破坏其中任意一个条件,就能避免死锁。
但数据库通常满足前三个条件,因此循环等待是死锁的根本原因。
三、死锁示例:经典案例
示例一:互相更新不同记录
-- 事务A
BEGIN;
UPDATE user SET age=age+1 WHERE id=1;-- 事务B
BEGIN;
UPDATE user SET age=age+1 WHERE id=2;-- 事务A 再次请求 id=2
UPDATE user SET age=age+1 WHERE id=2;-- 事务B 再次请求 id=1
UPDATE user SET age=age+1 WHERE id=1;
执行顺序如下
A 锁住 id=1
B 锁住 id=2
A 等待 B 释放 id=2
B 等待 A 释放 id=1
→ 死锁形成
InnoDB 检测到循环等待后,会回滚其中一个事务:
ERROR 1213 (40001): Deadlock found when trying to get lock
示例二:范围更新导致间隙锁死锁
-- 事务A
SELECT * FROM user WHERE age BETWEEN 10 AND 20 FOR UPDATE;-- 事务B
SELECT * FROM user WHERE age BETWEEN 15 AND 25 FOR UPDATE;
锁范围重叠 (15~20),
→ A、B 分别锁定一部分区间,彼此等待对方释放 → 死锁。
四、InnoDB 死锁检测机制
InnoDB 内部维护一张 等待图(Wait-for Graph):
- 每个事务是一个节点;
- 每个锁等待是一个有向边。
当出现环路时,即检测到死锁。
算法流程:
1. 事务 T1 等待 T2 → 建立边 T1 → T2
2. 事务 T2 等待 T3 → 建立边 T2 → T3
3. 若出现 T3 → T1,形成环路 → 死锁!
4. InnoDB 自动回滚代价最小的事务。
检测频率:实时检测(每次锁等待)
代价评估标准:
- 锁数量最少;
- 修改行数最少;
- 事务时间最短。
五、死锁日志分析
查看命令:
SHOW ENGINE INNODB STATUS\G
输出示例:
LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION:
TRANSACTION 145, ACTIVE 3 sec
UPDATE user SET age=30 WHERE id=1;*** (2) TRANSACTION:
TRANSACTION 146, ACTIVE 3 sec
UPDATE user SET age=30 WHERE id=2;*** WE ROLL BACK TRANSACTION (2)
分析要点:
- 哪个事务先加锁;
- 哪个事务被回滚;
- 涉及的表、索引、SQL。
六、避免死锁的最佳实践
① 保持统一的加锁顺序
所有事务按相同的字段顺序访问资源。
-- 一致加锁顺序
UPDATE user SET age=age+1 WHERE id IN (1,2);
② 减少锁定范围
尽量使用主键或唯一索引精确定位行。
-- 好:单行锁
SELECT * FROM user WHERE id=1 FOR UPDATE;
-- 坏:范围锁,死锁概率大
SELECT * FROM user WHERE age>10 FOR UPDATE;
③ 控制事务粒度与时长
- 拆小事务;
- 尽快提交;
- 避免长时间持锁操作(如网络等待、用户交互)。
④ 合理选择事务隔离级别
- 在允许的场景下使用 READ COMMITTED;
- 可减少间隙锁带来的死锁几率。
⑤ 加锁顺序与索引命中调试
- 检查执行计划(EXPLAIN);
- 确保条件使用索引,否则会退化为表锁。
七、面试高频问题与答题模板
| 问题 | 答案要点 |
|---|---|
| Q1:死锁的四个形成条件? | 互斥、占有且等待、不可剥夺、循环等待。 |
| Q2:InnoDB 如何检测死锁? | 构建等待图,检测环路。 |
| Q3:发现死锁后 MySQL 怎么处理? | 自动回滚代价最小的事务。 |
| Q4:死锁常见场景? | 不同事务交叉更新、范围锁重叠。 |
| Q5:如何避免死锁? | 统一加锁顺序、减小事务范围、索引命中、缩短事务时长。 |
| Q6:如何排查死锁? | 使用 SHOW ENGINE INNODB STATUS\G 查看日志。 |
八、总结
死锁的本质是循环等待资源。
它不是“异常错误”,而是并发控制中的自然现象。
真正重要的是——
设计出能快速检测、自动回滚、尽量规避的事务逻辑。
一句话记住:
锁太多不一定死,但乱锁一定死。
下一篇(第 15 篇),我将写——
《Explain 执行计划详解:SQL 性能瓶颈与索引命中分析》,
讲清楚执行计划每一列(type、key、rows、Extra)的意义,
并教你如何用 Explain 定位慢查询瓶颈。
