死锁产生与排查

一句话总结

死锁 = 事务 A 拿着 1 号资源等 2 号,事务 B 拿着 2 号资源等 1 号。两个人互相等对方松手,谁也干不完,谁也退不了。

MySQL 怎么处理死锁?

死锁发生后,MySQL 不会永远等下去。它有死锁检测机制:


graph TD

    A[事务 A] -->|持有 id=1,等待 id=2| B[事务 B]

    B -->|持有 id=2,等待 id=1| A

    A -.->|循环等待| B

    style A fill:#ffcccc

    style B fill:#ffcccc

死锁不会让整个系统卡死,MySQL 会自动杀掉一个事务。但代价是那个事务的改动被回滚,应用层需要重试。

最常见的死锁场景

场景 1:两条记录加锁顺序不一致(最典型)

 
-- ❌ 死锁风险
 
-- 事务 A
 
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
 
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
 
-- 事务 B
 
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
 
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
 
-- 如果 A 锁了 1、B 锁了 2 → 互相等 → 死锁
 
 
-- ✅ 修复:按相同顺序加锁
 
-- 两个事务都先锁 id=1,再锁 id=2
 
-- 不管转账方向如何,先锁小 id,再锁大 id
 

最推荐的修复方式:所有事务按同一顺序访问资源。

场景 2:间隙锁导致的死锁

关键认知:间隙锁不互斥。 两个事务可以同时持有同一个间隙的间隙锁。


sequenceDiagram

    participant A as 事务 A

    participant B as 事务 B

    Note over A,B: 现有数据:age=5, age=25(中间无数据)

    A->>A: 加间隙锁 (10, 20) ✅

    B->>B: 加间隙锁 (10, 20) ✅(间隙锁不互斥)

    A--xB: INSERT INTO user (age) VALUES (15)<br/>需要插入意向锁,等 B 释放间隙锁

    B--xA: INSERT INTO user (age) VALUES (15)<br/>需要插入意向锁,等 A 释放间隙锁

    Note over A,B: 死锁!A 等 B,B 等 A

核心矛盾:两个事务都能拿到同一个间隙的锁(间隙锁不互斥),但都不能插入(插入意向锁互斥)。互相等对方先提交/回滚释放间隙锁,于是死锁。

缓解:尽量用等值条件而不是范围条件,减少间隙锁的持有范围。

场景 3:主键冲突导致的死锁

 
-- 事务 A
 
INSERT INTO user (id, name) VALUES (1, 'Alice');
 
-- 还没提交
 
-- 事务 B
 
INSERT INTO user (id, name) VALUES (1, 'Bob');
 
-- 等待事务 A 释放 id=1(主键冲突)
 
-- 事务 C
 
INSERT INTO user (id, name) VALUES (2, 'Charlie');
 
-- 正常,和 id=1 没关系
 
-- 但如果有外键或其它锁关联关系,可能让 C 也卷进来
 

Step 1:查看 SHOW ENGINE INNODB STATUS 的输出

事务 1:

  • TRANSACTION 2001, ACTIVE 10 sec

  • MySQL thread id 5, query id 100

  • UPDATE accounts SET balance = balance - 100 WHERE id = 1

  • 持有锁: Record lock, space id 10, page no 5, n bits 72 → 持有 id=1 的行锁

  • 等待锁: Record lock, space id 10, page no 5, n bits 72 → 在等 id=2 的锁

事务 2:

  • TRANSACTION 2002, ACTIVE 8 sec

  • MySQL thread id 6, query id 101

  • UPDATE accounts SET balance = balance - 50 WHERE id = 2

  • 持有锁: Record lock, space id 10, page no 5, n bits 72 → 持有 id=2 的行锁

  • 等待锁: Record lock, space id 10, page no 5, n bits 72 → 在等 id=1 的锁

结果: MySQL 选择了事务 2 回滚(WE ROLL BACK TRANSACTION (2)

Step 2:看哪个 SQL 产生的

 
-- 开启死锁日志
 
SET GLOBAL innodb_print_all_deadlocks = ON;
 
-- 之后每次死锁都会记录到 MySQL error log
 
-- 直接看日志文件里的死锁信息和 SQL
 

Step 3:分析业务代码

从死锁日志找到两条 SQL → 定位到代码位置

发现:

  • 事务 A:转账 1→2(先锁 1 再锁 2)

  • 事务 B:转账 2→1(先锁 2 再锁 1)

修复:两个事务都先锁小 id 再锁大 id

一句话讲清

延伸提问:“死锁怎么产生的?怎么排查?”

“死锁是事务之间循环等待资源。最常见的是多条记录加锁顺序不一致——A 锁 1 等 2,B 锁 2 等 1。

排查分三步:

  1. SHOW ENGINE INNODB STATUS 看最后一次死锁
  1. 开启 innodb_print_all_deadlocks,监控错误日志
  1. 从日志里找到两条冲突 SQL,定位代码位置

预防最有效的是所有事务按相同顺序加锁,比如转账无论方向都先锁小 id。另外事务尽量短、隔离级别别太高。如果死锁不可避免,应用层要做好重试。“


记忆口诀

死锁 = 你等我我等你,两个人僵住了。

MySQL 自动杀一个,另一个正常走,杀的那个要重试。

SHOW ENGINE INNODB STATUS 看现场。

预防就一句话:锁的顺序固定好,事务别太长。

速记卡(面试闪卡)

Q1:一句话讲清「死锁产生与排查」到底是什么?

A:死锁就是两个事务互相拿着对方要的资源死等、谁也不松手,MySQL 会挑一个回滚让另一个成功。

Q2:死锁是怎么发生的? —— 怎么理解?

A:像两个人吃饭各拿一只筷子等对方那只——谁都不肯先放,就永远吃不上。对应英文 Deadlock(死锁)= 两个及以上事务互相持有对方需要的资源并循环等待,谁都推进不了。

Q3:MySQL 遇到死锁怎么办? —— 怎么理解?

A:像交警看到两车顶牛,直接把其中一辆拖走让路通——MySQL 的死锁检测(deadlock detection)会挑一个代价小的事务回滚,被回滚的应用层重试即可,系统不会卡死。

Q4:最常见的死锁场景是什么? —— 怎么理解?

A:像两人转账一个先锁 A 再锁 B、另一个先锁 B 再锁 A,顺序反了就顶牛。还有 Gap Lock(间隙锁,不互斥)下两个事务都能拿到同一间隙锁、却都插不进去,也会死锁。

Q5:怎么排查和预防死锁? —— 怎么理解?

A:像查车祸先看监控——SHOW ENGINE INNODB STATUS 看最后一次死锁现场,开 innodb_print_all_deadlocks 记日志,再从 SQL 定位代码。预防就一句话:所有事务按相同顺序加锁、事务别太长。

Q6:核心速记主线有哪些?

  • 死锁本质:事务间循环等待资源,你等我我等你

  • MySQL 自动杀一个事务回滚,另一个正常走,被杀的要重试

  • 最常见坑:加锁顺序不一致、间隙锁导致死锁

  • 预防核心:统一加锁顺序、短事务、必要时重试

口诀

A:死锁就是两人杠,你拿我叉我不放。

MySQL 当交警,拖走一个路通畅。

排查先看 INNODB,日志打开记现场。

顺序固定事务短,重试一下无大妨。

相关链接