死锁(Deadlock)是多个事务因循环等待锁资源而永久阻塞的现象。它虽然危险,但并不可怕,MySQL InnoDB 有自动检测和恢复机制。
🚨 死锁是如何产生的?
死锁的产生必须同时满足四个条件,InnoDB 中的死锁通常源于以下两种典型场景:
- 不同表,相反顺序:事务A更新表1再更新表2,事务B更新表2再更新表1,形成循环等待。
- 相同表,不同范围:事务A通过范围条件锁定一个间隙,事务B锁定另一个间隙,随后各自尝试插入对方锁定的间隙,导致死锁。
注意:死锁主要由写操作(UPDATE,DELETE,INSERT,SELECT ... FOR UPDATE)引起,其发生概率不受隔离级别直接影响。
🔍 MySQL InnoDB 如何处理死锁?
MySQL InnoDB 提供了自动处理机制,但在极端高并发下也可能需要人工干预。
- 自动检测与回滚:默认开启。InnoDB 会构建等待图(Wait-for Graph),检测到环路后,会回滚一个事务(“受害者”)来打破死锁。通常选择修改行数较少的事务作为回滚对象。
- 兜底超时机制:若禁用自动检测,或死锁涉及外部锁,则依赖
innodb_lock_wait_timeout(默认50秒)。事务等待超时后会自动回滚。 - 深度检测限制:当锁等待关系过于复杂(如超过200个事务),InnoDB 会直接回滚当前事务,避免性能崩溃。
🛠️ 如何排查死锁?
死锁排查的核心是获取和分析死锁日志。
获取死锁日志
- 查看最近一次死锁:执行
SHOW ENGINE INNODB STATUS\G,在输出中查找LATEST DETECTED DEADLOCK部分。 - 记录所有死锁(推荐):开启
innodb_print_all_deadlocks = ON,将所有死锁记录到 MySQL 错误日志,便于长期追踪。
- 查看最近一次死锁:执行
分析死锁日志
日志会清晰展示死锁的完整链条。- 事务信息:
TRANSACTION块展示了事务ID和状态。 - 持有的锁:
HOLDS THE LOCK(S)部分显示该事务当前成功获取的锁。 - 等待的锁:
WAITING FOR THIS LOCK部分显示该事务被阻塞的锁请求。 - 回滚决策:日志末尾会标明哪个事务被回滚(
WE ROLL BACK TRANSACTION)。
- 事务信息:
定位根因
分析日志中的 SQL 语句和锁资源,找出循环等待的路径。通常根因是事务以不一致的顺序访问资源,或锁定的范围有重叠。
🛡️ 如何预防死锁?
预防优于处理,以下是最佳实践:
- 强制统一访问顺序:所有事务按相同顺序(如主键升序)访问表和行。
- 保持事务短小精悍:尽快提交,减少锁持有时间。
- 使用合适的索引:确保
WHERE条件能命中索引,避免行锁升级为表锁。 - 考虑降低隔离级别:若业务允许,使用
READ COMMITTED级别可减少间隙锁,降低死锁概率。 - 使用
INSERT ... ON DUPLICATE KEY UPDATE:合并先查后改的逻辑,减少锁请求次数。
🏥 如何从死锁中恢复?
死锁是并发场景下的常态,需要在应用层优雅处理。
- 捕获并重试:这是最核心的恢复策略。在代码中捕获死锁异常(
Deadlock found when trying to get lock; try restarting transaction),并进行有限次数的重试(通常1-2次即可成功)。
💎 总结
- 死锁本质:多个事务因循环等待锁而无限阻塞。
- InnoDB策略:默认自动检测并回滚“代价最小”的事务。
- 核心对策:统一资源访问顺序+缩短事务+应用层重试。
- 不要害怕:死锁是并发数据库的正常现象,设计健壮的重试机制是解决问题的关键。