如何避免 MySQL 死锁?——从原理到实战的系统性解决方案

在高并发业务中,MySQL 死锁几乎是绕不开的问题。

你可能遇到过这样的报错:

java 复制代码
Deadlock found when trying to get lock; try restarting transaction

死锁并不是 MySQL 的 Bug,而是并发设计不当的必然结果。

本文将从 死锁原理、常见场景、排查方式、设计规范、Java 实战 五个维度,系统讲清楚:MySQL 死锁如何避免?

一、什么是 MySQL 死锁?

  1. 死锁的定义

死锁(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;

六、总结(架构级结论)

死锁不是偶发事故,而是并发设计问题。

一句话记住:

  1. 统一顺序
  2. 索引优先
  3. 事务要短
  4. 范围要小
  5. 必要可重试
  6. 热点做串行

📌 优秀的系统不是"没有死锁",而是"死锁可控、可恢复、不影响业务"。

相关推荐
Decay__4 小时前
05_MySQL 常用 SQL 大全 —— 诊断、管理、优化辅助 SQL 与三大系统库速查
运维·数据库·sql·mysql·架构
小徐不徐说8 小时前
C++ 接口与插件设计:从工厂注册到 Qt 动态插件
数据库·c++·qt·系统架构·程序设计
论文复现现场9 小时前
WRF、FEA 能用 L40S 加速吗?求解器检查、断点恢复与长期运行
java·数据库·算法·wrf·有限元分析·gpu计算·断点续算
Decay__10 小时前
04_MySQL DBA 日常运维手册 —— 会话管理、锁与阻塞、事务监控、慢查询与日志巡检
运维·数据库·sql·mysql·架构
心梦缘-雪雁11 小时前
SQL Server 查询数据存在哪个表里
数据库
lusklusklusk12 小时前
Python 巡检脚本:进程运行状态、数据库写入时效检查及邮件通知与告警
数据库·python·oracle
GeW12 小时前
产业新优势基础软件的人才底盘,从Linux与数据库开始
linux·数据库
SelectDB12 小时前
Doris Arrow Flight SQL 高速读取协议实战:与 Snowflake 客户端取数链路对照
大数据·数据库·数据分析
曼岛_12 小时前
[图解密码学]第4章 古典密码
java·数据库·密码学
瀚高PG实验室13 小时前
HGDB V9.0.5 .pgpass 免密登录提示 IsEncryptNon-base64 encoding 警告处理
数据库·postgresql·瀚高数据库