死锁

死锁

死锁,不是“锁死了”,而是两个或两个以上的事务,互相等待着对方释放自己需要的锁,形成了一个闭环,导致谁都无法继续执行

常见死锁场景

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。让事务在一开始就拿到需要的排他锁,而不是说“我先看看”,之后再升级,这能避开共享锁升级带来的死锁问题。

posted @ 2026-05-07 16:57  xzlrf  阅读(4)  评论(0)    收藏  举报