跳到主要内容

死锁

问题

什么是死锁?数据库死锁是如何产生的?如何检测、排查和预防死锁?

答案

什么是死锁

死锁(Deadlock)是指 两个或多个事务互相持有对方需要的锁,导致所有事务都无法继续执行的状态。

死锁的四个必要条件

条件说明数据库中的体现
互斥资源一次只能被一个事务持有排他锁(X Lock)
持有并等待持有锁的同时等待其他锁事务内有多次加锁操作
不可抢占已持有的锁不能被强制释放锁只在事务结束时释放
循环等待事务之间形成等待环路A 等 B,B 等 A

死锁的典型场景

场景 1:交叉更新

-- 事务 A                              -- 事务 B
BEGIN; BEGIN;
UPDATE accounts SET balance = 900
WHERE id = 1; -- 锁住行 1
UPDATE accounts SET balance = 900
WHERE id = 2; -- 锁住行 2

UPDATE accounts SET balance = 1100
WHERE id = 2; -- 等待行 2 的锁 ⏳
UPDATE accounts SET balance = 1100
WHERE id = 1; -- 等待行 1 的锁 ⏳
-- 💀 死锁!

场景 2:间隙锁冲突(MySQL RR 特有)

-- 表 t: id = 1, 5, 10
-- 事务 A -- 事务 B
BEGIN; BEGIN;
SELECT * FROM t WHERE id = 3
FOR UPDATE;
-- 加间隙锁 (1, 5)
SELECT * FROM t WHERE id = 7
FOR UPDATE;
-- 加间隙锁 (5, 10)

INSERT INTO t VALUES (4);
-- 需要插入到 (1,5) 区间
-- 被 B 的间隙锁阻塞 ⏳
INSERT INTO t VALUES (6);
-- 需要插入到 (5,10) 区间
-- 被 A 的间隙锁阻塞 ⏳
-- 💀 死锁!

场景 3:索引顺序不同

-- 假设 name 和 age 分别有索引
-- 两个事务通过不同索引锁定同一行,但加锁顺序不同

-- 事务 A: 先通过 name 索引锁定,再通过 age 索引锁定
-- 事务 B: 先通过 age 索引锁定,再通过 name 索引锁定

InnoDB 的死锁处理

死锁检测

InnoDB 默认开启 wait-for graph(等待图) 死锁检测:

回滚策略:InnoDB 选择 undo log 最少的事务 回滚(代价最小)。

相关参数

-- 查看死锁检测是否开启
SHOW VARIABLES LIKE 'innodb_deadlock_detect';
-- ON(默认开启)

-- 锁等待超时(秒)
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
-- 50(默认 50 秒)
高并发下的死锁检测性能

死锁检测需要遍历等待图,时间复杂度 O(n2)O(n^2)。在热点行高并发更新时(如秒杀扣库存),大量事务等待同一行锁,死锁检测本身会消耗大量 CPU。

优化方案:

  1. 关闭死锁检测(innodb_deadlock_detect = OFF),依赖 innodb_lock_wait_timeout 超时
  2. 减少热点行竞争:分桶库存、队列削峰

死锁排查

查看最近的死锁信息

-- 查看最后一次死锁
SHOW ENGINE INNODB STATUS\G
-- 在 LATEST DETECTED DEADLOCK 部分查看

输出示例解读:

*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 2 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 100, query id 5678
UPDATE accounts SET balance=900 WHERE id=2

*** (1) HOLDS THE LOCK(S): -- 事务1持有的锁
RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY
-- 持有 id=1 的行锁

*** (1) WAITING FOR THIS LOCK: -- 事务1等待的锁
RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY
-- 等待 id=2 的行锁

*** (2) TRANSACTION:
-- 事务2 持有 id=2 的锁,等待 id=1 的锁

*** WE ROLL BACK TRANSACTION (2) -- 被回滚的事务

监控死锁

-- MySQL 8.0+ 查看锁等待
SELECT * FROM performance_schema.data_lock_waits;

-- 查看当前正在等待的锁
SELECT * FROM performance_schema.data_locks
WHERE LOCK_STATUS = 'WAITING';

-- 查看事务信息
SELECT * FROM information_schema.innodb_trx;

死锁预防策略

策略说明效果
固定加锁顺序多个表/行总是按相同顺序操作消除循环等待
减少事务范围事务尽量短,减少持锁时间减少冲突概率
合理使用索引确保 UPDATE/DELETE 走索引避免锁全表
降低隔离级别使用 RC 代替 RR消除间隙锁
避免大事务不要在一个事务中操作太多行减少锁数量
乐观锁用版本号替代排他锁避免长时间持锁

固定加锁顺序

-- ❌ 可能死锁:事务 A 先锁 1 再锁 2,事务 B 先锁 2 再锁 1
-- 事务 A
UPDATE accounts SET ... WHERE id = 1;
UPDATE accounts SET ... WHERE id = 2;
-- 事务 B
UPDATE accounts SET ... WHERE id = 2;
UPDATE accounts SET ... WHERE id = 1;

-- ✅ 统一按 id 升序加锁
-- 事务 A 和 B 都先锁 1 再锁 2
UPDATE accounts SET ... WHERE id = 1;
UPDATE accounts SET ... WHERE id = 2;

乐观锁替代

-- 用 version 字段做乐观锁,避免 SELECT FOR UPDATE
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 100 AND version = 5;
-- 如果返回 affected rows = 0,表示有冲突,重试

各数据库的死锁处理

数据库检测机制处理方式
MySQL InnoDB等待图检测回滚 undo 最小的事务
PostgreSQL等待图检测回滚最后申请锁的事务
Oracle等待图检测回滚语句(不是整个事务)
SQL Server锁监控线程回滚资源消耗最小的事务

常见面试问题

Q1: 如何排查线上死锁?

答案

  1. 查看死锁日志SHOW ENGINE INNODB STATUS,找到 LATEST DETECTED DEADLOCK
  2. 分析两个事务:找到各自持有的锁和等待的锁
  3. 定位 SQL:找到引起死锁的具体 SQL 语句
  4. 分析加锁顺序:检查两个事务对同一组行的加锁顺序是否相反
  5. 修复:统一加锁顺序 / 拆分事务 / 降低隔离级别

Q2: 如何减少死锁的发生?

答案

  1. 统一加锁顺序:多表操作时按固定顺序(如表名字母序、主键升序)
  2. 缩短事务:事务中只包含必要操作,不要在事务中执行慢查询或外部调用
  3. 使用合理索引:确保 WHERE 条件走索引,避免行锁升级为表锁
  4. RC 替代 RR:RC 没有间隙锁,减少间隙锁死锁
  5. 乐观锁:对于冲突率低的场景,用 version 字段替代 FOR UPDATE
  6. 应用层重试:死锁回滚后自动重试

Q3: MySQL 是如何检测死锁的?

答案

InnoDB 使用 等待图(Wait-for Graph) 算法:

  1. 每当有事务开始等待锁时,在等待图中添加边(A → B 表示 A 等待 B 持有的锁)
  2. 检测图中是否存在 环路(cycle)
  3. 如果有环路 → 死锁
  4. 选择 undo log 量最少的事务回滚

检测是即时的(每次加锁等待都触发),所以死锁一旦发生会立即被检测到,不需要等超时。

Q4: 间隙锁为什么容易造成死锁?

答案

间隙锁是 RR 级别下为了防止幻读而引入的锁,锁定的是一个范围(而不是具体的行)。

间隙锁之间不互斥(两个事务可以同时锁住同一个间隙),但间隙锁会与插入意向锁互斥。

典型死锁场景:

  1. 事务 A 对范围 (5, 10) 加间隙锁
  2. 事务 B 对范围 (5, 10) 加间隙锁(不冲突)
  3. 事务 A 要在 (5, 10) 范围插入 → 被 B 的间隙锁阻塞
  4. 事务 B 也要在 (5, 10) 范围插入 → 被 A 的间隙锁阻塞
  5. 死锁!

解决:降级到 RC 级别(不使用间隙锁),或调整业务逻辑避免并发插入。

Q5: 死锁和锁等待超时有什么区别?

答案

对比死锁锁等待超时
原因循环依赖,永远无法解开长事务持锁不释放
检测方式等待图检测(即时)超时检测(默认 50s)
处理方式回滚一个事务等待超时报错
错误码ERROR 1213 (Deadlock)ERROR 1205 (Lock wait timeout)
可避免性可以通过设计避免可以通过缩短事务避免

相关链接