MySQL InnoDB 死锁全解析:4 大必要条件、典型场景与规避策略

3 阅读4分钟

MySQL InnoDB 死锁

目录

  • 一、什么是死锁
  • 二、死锁发生的 4 个必要条件(重点)
  • 三、死锁怎么发生
  • 四、InnoDB 里常见的死锁场景
  • 五、怎么避免死锁?

一、什么是死锁

死锁就是两个或多个事务,互相持有对方需要的锁,又都在等待对方释放锁,导致谁也动不了,无限等待的状态。

—— 一条车道,两台小车对向堵着,都想着对面让道,谁也不让谁。

二、死锁发生的 4 个必要条件(重点)

这四个条件同时满足,才会产生死锁:

  1. 互斥条件:资源(锁)只能被一个事务持有,其他事务必须等待。
  2. 请求与保持条件:事务已经持有了一些锁,又去请求新的锁,且不释放已有的锁。
  3. 不剥夺条件:已经持有的锁不能被强行剥夺,只能等事务自己释放。
  4. 循环等待条件:多个事务之间形成一个等待环路,A 等 B,B 等 A。

只要破坏其中任意一个条件,死锁就不会发生。

三、死锁怎么发生

假设这时有两事务,一个事务要插入订单 1007 ,另外一个事务要插入订单 1008,因为需要对订单做幂等性校验,所以两个事务先要查询该订单是否存在,不存在才插入记录,过程如下:

  • 事务 A:检查订单 1007 是否存在 → 加了范围锁 (1006, +∞)
  • 事务 B:检查订单 1008 是否存在 → 加了范围锁 (1006, +∞)
  • 事务 A:尝试插入订单 1007 → 需要申请插入意向锁,被事务 B 的范围锁阻塞
  • 事务 B:尝试插入订单 1008 → 需要申请插入意向锁,被事务 A 的范围锁阻塞

死锁形成

  1. 互斥:范围锁和插入意向锁互斥,不能同时持有。
  2. 请求与保持:A 持有 (1006, +∞) 的锁,又请求插入意向锁;B 持有 (1006, +∞) 的锁,又请求插入意向锁。
  3. 不剥夺:锁不能被强行抢走,只能等事务自己释放。
  4. 循环等待:A 等 B 释放锁,B 等 A 释放锁,形成环路。

这样就卡住了。

四、InnoDB 里常见的死锁场景

模式 1:交叉加锁(最经典)

  • 事务 A:先锁记录 1,再锁记录 2
  • 事务 B:先锁记录 2,再锁记录 1
  • 结果:A 等 B 释放记录 2,B 等 A 释放记录 1,形成环路。

模式 2:间隙锁 + 插入意向锁

  • 事务 A:用 select ... for update 加了范围锁 (1006, +∞)
  • 事务 B:也用 select ... for update 加了范围锁 (1006, +∞)
  • 事务 A:尝试插入 1007,需要插入意向锁,被 B 的范围锁阻塞
  • 事务 B:尝试插入 1008,需要插入意向锁,被 A 的范围锁阻塞
  • 结果:互相等待,死锁。

五、怎么避免死锁?

核心思路就是破坏死锁的四个必要条件

  1. 按固定顺序访问资源

    • 比如所有事务都先锁记录 1,再锁记录 2,避免交叉加锁。
  2. 尽量使用低级别的隔离级别

    • 比如用读已提交(RC)代替可重复读(RR),减少间隙锁的使用,降低死锁概率。
  3. 让事务尽快提交

    • 事务持有锁的时间越短,死锁的窗口就越小。
  4. 设置锁等待超时

    • 通过 innodb_lock_wait_timeout 设置等待时间,超时后自动回滚事务,打破死锁。
  5. 检测并主动处理死锁

    • InnoDB 有死锁检测机制,发现死锁后会主动回滚代价最小的事务,让其他事务继续执行。

针对 InnoDB 特性的优化

  1. 避免在同一时间点运行多个对同一表进行读写的脚本

    • 如果必须并行,可以设置一些定时脚本,让它们在不同的时间点运行,错开高峰。
    • 比如:脚本 A 在 0 点运行,脚本 B 在 0:30 运行,避免同时争抢锁。
  2. 一次性锁定所需资源(模仿 MyISAM 表锁思想)

    • MyISAM 只支持表锁,一次锁住所有资源,避免了行锁导致的死锁。
    • 在 InnoDB 中,可以在事务开始时,用 SELECT ... FOR UPDATE 一次性锁定所有需要的记录,避免多次加锁导致的循环等待。
  3. 利用死锁检测机制

    • InnoDB 内置了死锁检测,发现死锁后会主动回滚「代价最小」的事务(通常是 undo log 最少的事务),让其他事务继续执行。
    • 可以通过 innodb_deadlock_detect 开启(默认开启),在高并发场景下,死锁检测本身也会消耗性能,可以根据情况权衡。