跳转至

MySQL 死锁分析思路

一、什么是死锁

两个或多个事务互相持有对方需要的锁,形成循环等待。

事务 A:持有 id=1 的行锁,等待 id=2 的行锁
事务 B:持有 id=2 的行锁,等待 id=1 的行锁

MySQL 检测到死锁后,会选一个事务作为牺牲者回滚。

二、死锁产生的必要条件

  1. 互斥:锁同一时刻只能被一个事务持有。
  2. 持有并等待:持有一把锁的同时等待另一把。
  3. 不可剥夺:锁只能由持有者主动释放。
  4. 循环等待:形成环形等待链。

三、常见死锁场景

1. 不同顺序更新同一组行

-- 事务 A
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;

-- 事务 B
BEGIN;
UPDATE account SET balance = balance - 50 WHERE id = 2;
UPDATE account SET balance = balance + 50 WHERE id = 1;

解决:所有事务按相同顺序访问行(如按 id 升序)。

2. 间隙锁(Next-Key Lock)冲突

可重复读隔离级别下,范围查询会加间隙锁:

-- 表 t 有 id 1, 5, 10
-- 事务 A
BEGIN;
SELECT * FROM t WHERE id > 5 AND id < 10 FOR UPDATE;
-- 锁住 (5, 10) 这个间隙

-- 事务 B
BEGIN;
INSERT INTO t(id) VALUES (7);  -- 阻塞

3. 唯一索引插入冲突

两个事务同时插入同一唯一键,都会尝试加锁,互相等待。

四、如何排查

1. 查看最近死锁日志

SHOW ENGINE INNODB STATUS;

LATEST DETECTED DEADLOCK 段会打印:

  • 持有锁的事务。
  • 等待锁的事务。
  • 两边执行的 SQL。
  • 回滚了哪个事务。

2. 打开锁监控

SET GLOBAL innodb_print_all_deadlocks = ON;

死锁会写到 error log。

3. 查看当前锁等待

SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;

五、如何避免

  1. 业务上按相同顺序访问表和行
  2. 大事务拆小:事务越长,持锁时间越久。
  3. 索引完善:没有索引会锁全表。
  4. 降低隔离级别:RC 不会加间隙锁(RR 才有)。
  5. 避免在事务中交互等待外部资源

六、死锁 vs 锁等待

死锁 锁等待
表现 互相等待,自动检测 单向等待
超时时间 立即检测 innodb_lock_wait_timeout(默认 50s)
处理 自动回滚一个 超时后报错

线上排查流程

  1. 拿到死锁日志,找两边 SQL。
  2. 分析 WHERE 条件走了什么索引、锁了哪些行。
  3. 调整访问顺序或 SQL,必要时改隔离级别。