死锁
死锁
死锁,不是“锁死了”,而是两个或两个以上的事务,互相等待着对方释放自己需要的锁,形成了一个闭环,导致谁都无法继续执行。
常见死锁场景
1. 最常见的场景:相反顺序加锁
两个事务,以不同的顺序更新同一批数据。
- 事务A:先锁住
id=1,然后想去锁id=2。 - 事务B:先锁住
id=2,然后想去锁id=1。
此时,A 等 B 释放 id=2 的锁,B 等 A 释放 id=1 的锁,死锁发生。
2. 隐式锁升级:共享锁与排他锁的冲突
- 事务A:先用
SELECT ... LOCK IN SHARE MODE读取了数据并获取了共享锁(S锁)。 - 事务B:也获取了同一行的共享锁(S锁),共享锁之间是兼容的。
- 接着,事务A 想执行
UPDATE,需要将共享锁升级为排他锁(X锁),它必须等待 B 释放其共享锁。 - 同时,事务B 也想执行
UPDATE,也需要升级锁并等待 A。
双方都持有对方的共享锁,又都在等对方的排他锁,形成死锁。
3. 在 REPEATABLE-READ 隔离级别下,由间隙锁引发的死锁
这是最隐蔽的。当事务使用 WHERE 条件进行加锁且未命中任何行时,会加间隙锁(Gap Lock)。
- 事务A:删除
id=5的数据,但id=5不存在,此时会在一个区间加上间隙锁。 - 事务B:插入
id=5的数据,会先在目标位置加上插入意向锁,然后等待间隙锁释放。 - 事务A:这时也想插入
id=5,同样等待插入意向锁。
此时,A、B 互相等待,形成死锁。间隙锁与插入意向锁不兼容,是高并发下 INSERT 死锁的元凶之一。
4. 唯一键冲突引发的死锁
- 事务A、B 同时尝试插入相同的唯一键值。
- 事务A 先插入,获取了该行的共享锁(S锁),并等待提交。
- 事务B 再插入,因唯一键冲突,也会获取一个共享锁等待。
- 如果事务A 此时回滚或执行了导致B需要升级为X锁的操作,双方就可能陷入等待。
查看死锁日志
当死锁发生时,InnoDB 会自动检测并选择一个代价较小的事务作为“受害者”进行回滚,报错:Deadlock found when trying to get lock; try restarting transaction。被回滚的事务里可以立刻重试。
要查看最近的死锁详情,使用下面的命令:
sql
SHOW ENGINE INNODB STATUS;
在输出的结果中,找到 LATEST DETECTED DEADLOCK 部分,它会详细记录:
- WAITING FOR THIS LOCK TO BE GRANTED:事务在等待什么锁。
- HOLDS THE LOCK(S):这个事务又持有什么锁。
通过分析这两个信息,你就能画出事务之间的等待环,找到死锁的根源。
避免方式:顺序加锁、小事务、超时
1. 统一加锁顺序:最重要的原则
确保应用里的所有事务,在需要更新多张表或多行数据时,都按照相同的顺序访问。比如都按主键 id 从小到大的顺序加锁,就能有效打破循环等待。
2. 缩短事务长度,尽早提交
事务开得越久,持有锁的时间就越长,与其他事务冲突的可能性就越大。
原则是:将不属于数据操作的计算、日志打印、远程调用等逻辑,全部移出事务。在事务内只做读写操作。
3. 善用索引,避免表扫描
死锁的根本是行锁。如果 WHERE 条件用不上索引,InnoDB 会进行全表扫描,并锁住大量甚至所有行。这极大地增加了冲突概率。确保相关字段有合理的索引。
4. 直接对想操作的数据加锁
在 SELECT 语句后加上 FOR UPDATE。让事务在一开始就拿到需要的排他锁,而不是说“我先看看”,之后再升级,这能避开共享锁升级带来的死锁问题。

浙公网安备 33010602011771号