在高并发业务中,MySQL 死锁几乎是绕不开的问题。
你可能遇到过这样的报错:
java
Deadlock found when trying to get lock; try restarting transaction
死锁并不是 MySQL 的 Bug,而是并发设计不当的必然结果。
本文将从 死锁原理、常见场景、排查方式、设计规范、Java 实战 五个维度,系统讲清楚:MySQL 死锁如何避免?
一、什么是 MySQL 死锁?
- 死锁的定义
死锁(Deadlock) 是指:
多个事务相互持有对方需要的锁,并且都在等待对方释放,导致所有事务永久阻塞。
经典四要素(缺一不可):
条件 说明
- 互斥 锁一次只能被一个事务持有
- 占有并等待 已持有锁的事务继续等待新锁
- 不可剥夺 锁只能由事务主动释放
- 循环等待 多个事务形成等待环
MySQL 的 InnoDB 引擎会主动检测死锁,并回滚代价最小的事务。
二、MySQL 中最常见的死锁场景
场景 1:不同顺序更新相同资源(最常见)
-- 事务 A
java
BEGIN;
UPDATE order SET status = 1 WHERE id = 1;
UPDATE order SET status = 1 WHERE id = 2;
-- 事务 B
java
BEGIN;
UPDATE order SET status = 2 WHERE id = 2;
UPDATE order SET status = 2 WHERE id = 1;
🔴 问题本质:
- A 先锁 id=1,再锁 id=2
- B 先锁 id=2,再锁 id=1
- 顺序不一致 → 循环等待
场景 2:范围更新 + 行更新(间隙锁)
-- 事务 A(范围锁)
java
UPDATE product SET stock = stock - 1 WHERE category_id = 10;
-- 事务 B(单行锁)
java
UPDATE product SET stock = stock - 1 WHERE id = 100;
🔴 在 RR 隔离级别 下:
范围更新会产生 Next-Key Lock(行锁 + 间隙锁)
容易与单行更新形成死锁
场景 3:SELECT ... FOR UPDATE 使用不当
java
SELECT * FROM account WHERE user_id = 1 FOR UPDATE;
如果:
- 没有命中索引
- 锁住大量行
- 多事务交叉执行
➡️ 极易引发死锁或长时间锁等待
场景 4:唯一索引插入并发冲突
java
INSERT INTO user(username) VALUES ('tom');
多事务并发插入相同唯一键
InnoDB 会先加 共享锁 → 排他锁
顺序不当也可能形成死锁
三、如何快速定位 MySQL 死锁?
1️⃣ 查看最近一次死锁信息(必会)
java
SHOW ENGINE INNODB STATUS;
重点关注:
java
LATEST DETECTED DEADLOCK
你可以看到:
- 哪些事务
- 执行了哪些 SQL
- 等待什么锁
- 持有什么锁
线上排查死锁的第一利器
2️⃣ 打开死锁日志(推荐)
java
SET GLOBAL innodb_print_all_deadlocks = 1;
死锁信息会直接写入 MySQL error log,方便线上分析。
四、避免 MySQL 死锁的 8 条核心原则(重点)
✅ 原则 1:统一访问顺序(最重要)
多表 / 多行更新,顺序必须一致
❌ 错误示例:
- A:订单 → 库存
- B:库存 → 订单
✅ 正确做法:
所有事务:订单 → 库存
✅ 原则 2:尽量使用主键 / 唯一索引更新
java
UPDATE order SET status = 1 WHERE id = ?;
避免:
- 全表扫描
- 范围锁
- 锁定多余行
✅ 原则 3:缩小事务范围(短事务)
❌ 错误:
java
@Transactional
public void process() {
select();
业务计算();
远程调用();
update();
}
✅ 正确:
java
select();
业务计算();
@Transactional
public void updateDb() {
update();
}
📌 事务只包数据库操作
✅ 原则 4:避免无索引的 SELECT FOR UPDATE
-- 错误(可能锁全表)
java
SELECT * FROM order WHERE status = 0 FOR UPDATE;
-- 正确
java
SELECT * FROM order WHERE id = ? FOR UPDATE;
✅ 原则 5:减少范围更新,必要时拆分
-- 不推荐
java
UPDATE order SET status = 1 WHERE create_time < '2024-01-01';
-- 推荐
分页 / 按主键批量更新
✅ 原则 6:合理设置隔离级别
如果业务允许:
java
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
减少间隙锁
显著降低死锁概率
✅ 原则 7:并发场景下控制重试机制
InnoDB 回滚后,应用层应:
java
try {
// db operation
} catch (DeadlockException e) {
// sleep + retry
}
📌 死锁不可怕,不可恢复才可怕
✅ 原则 8:热点资源做串行化设计
例如:
- 库存扣减
- 账户余额
同一订单状态流转
可选方案:
- Redis 分布式锁
- 消息队列串行消费
- 乐观锁(version)
五、Java 高并发场景下的实战建议
1️⃣ 使用乐观锁代替悲观锁
java
UPDATE product
SET stock = stock - 1, version = version + 1
WHERE id = ? AND version = ?;
失败则重试,避免大量锁竞争。
2️⃣ 库存 / 金额类操作单线程化
java
MQ → 单消费者 → DB
这是电商、物流系统的常规做法。
3️⃣ 避免"先查再改"的经典坑
❌
java
SELECT stock FROM product WHERE id = 1;
UPDATE product SET stock = stock - 1 WHERE id = 1;
✅
java
UPDATE product SET stock = stock - 1 WHERE id = 1 AND stock > 0;
六、总结(架构级结论)
死锁不是偶发事故,而是并发设计问题。
一句话记住:
- 统一顺序
- 索引优先
- 事务要短
- 范围要小
- 必要可重试
- 热点做串行
📌 优秀的系统不是"没有死锁",而是"死锁可控、可恢复、不影响业务"。