1、简述
死锁(Deadlock)是指两个或多个事务在执行过程中,因争夺锁资源而造成的一种相互等待的现象,若无外力干预,这些事务将永远无法继续执行。
在 MySQL(主要是 InnoDB 存储引擎)中,死锁是正常现象 ,并非 Bug。InnoDB 具备死锁检测机制 ,当检测到死锁时,会主动回滚代价较小的事务,并返回错误:
text
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

2、MySQL 中的锁基础(死锁的前提)
常见锁类型:
- 行锁(Record Lock)
- 间隙锁(Gap Lock)
- 临键锁(Next-Key Lock = 行锁 + 间隙锁)
- 意向锁(Intention Lock)
⚠️ 死锁通常发生在 行级锁 + 并发事务 场景下。
3、死锁产生的典型场景
场景 1:不同事务,反序加锁(最经典)
示例表
sql
CREATE TABLE account (
id INT PRIMARY KEY,
balance INT
) ENGINE=InnoDB;
INSERT INTO account VALUES (1, 100), (2, 100);
事务 A
sql
BEGIN;
UPDATE account SET balance = balance - 10 WHERE id = 1;
-- 等待
UPDATE account SET balance = balance + 10 WHERE id = 2;
事务 B
sql
BEGIN;
UPDATE account SET balance = balance - 10 WHERE id = 2;
-- 等待
UPDATE account SET balance = balance + 10 WHERE id = 1;
死锁分析
| 事务 | 已持有锁 | 正在等待 |
|---|---|---|
| A | id=1 | id=2 |
| B | id=2 | id=1 |
👉 形成环形等待 → 死锁
场景 2:范围查询 + 更新(间隙锁)
sql
BEGIN;
SELECT * FROM orders WHERE user_id = 100 FOR UPDATE;
sql
BEGIN;
INSERT INTO orders(user_id, amount) VALUES (100, 99);
- 第一个事务加了 Next-Key Lock
- 第二个事务插入被间隙锁阻塞
- 若双方后续又请求对方持有的锁 → 死锁
场景 3:唯一索引冲突 + 并发插入
sql
CREATE TABLE user (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(50) UNIQUE
) ENGINE=InnoDB;
sql
INSERT INTO user(email) VALUES ('a@test.com');
多个事务同时插入相同唯一键,可能引发死锁(而不是简单的唯一键冲突)。
4、如何定位 MySQL 死锁?
1️⃣ 查看最近一次死锁信息(最重要)
sql
SHOW ENGINE INNODB STATUS;
关注内容:
LATEST DETECTED DEADLOCK- 哪些 SQL
- 哪些索引
- 哪些事务被回滚
text
*** (1) TRANSACTION:
UPDATE account SET balance = balance - 10 WHERE id = 1
*** (2) TRANSACTION:
UPDATE account SET balance = balance - 10 WHERE id = 2
2️⃣ 开启死锁日志(线上推荐)
sql
SET GLOBAL innodb_print_all_deadlocks = ON;
死锁信息会输出到 MySQL error log,便于长期分析。
3️⃣ 监控指标
Innodb_deadlocksInnodb_row_lock_waitsInnodb_row_lock_time
5、MySQL 死锁解决方案
✅ 方案 1:统一加锁顺序(最有效)
原则:多个事务访问多行数据时,保证顺序一致
❌ 错误示例:
text
事务 A:锁 1 → 锁 2
事务 B:锁 2 → 锁 1
✅ 正确示例:
text
事务 A:锁 1 → 锁 2
事务 B:锁 1 → 锁 2
✅ 方案 2:缩小事务范围,减少锁持有时间
❌
sql
BEGIN;
SELECT ...;
-- 业务逻辑(耗时)
UPDATE ...
COMMIT;
✅
sql
-- 业务逻辑
BEGIN;
UPDATE ...
COMMIT;
✅ 方案 3:使用合适索引,避免全表/范围锁
❌ 无索引导致:
sql
UPDATE orders SET status = 1 WHERE user_id = 100;
👉 可能锁大量记录甚至产生间隙锁
✅ 建立索引:
sql
CREATE INDEX idx_user_id ON orders(user_id);
✅ 方案 4:降低隔离级别(谨慎)
sql
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
- 减少 Gap Lock
- 适用于 读多写少 场景
- ⚠️ 注意是否能接受不可重复读
✅ 方案 5:应用层重试(必须)
InnoDB 会自动回滚一个事务,业务应当:
- 捕获
Deadlock found异常 - 进行 有限次数重试
java
for (int i = 0; i < 3; i++) {
try {
doTransaction();
break;
} catch (DeadlockException e) {
if (i == 2) throw e;
}
}
6、实践样例
sql
BEGIN;
-- 按 id 顺序加锁,避免死锁
SELECT balance FROM account WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
UPDATE account SET balance = balance - 10 WHERE id = 1;
UPDATE account SET balance = balance + 10 WHERE id = 2;
COMMIT;
关键点:
FOR UPDATEORDER BY- 保证锁顺序一致
7、总结
死锁的本质: 并发事务 + 锁 + 不一致的资源获取顺序
- ✅ 统一加锁顺序
- ✅ 缩短事务
- ✅ 合理索引
- ✅ 控制隔离级别
- ✅ 应用层重试
- ✅ 监控 & 日志分析