MySQL:死锁问题分析与解决方案

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_deadlocks
  • Innodb_row_lock_waits
  • Innodb_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 UPDATE
  • ORDER BY
  • 保证锁顺序一致

7、总结

死锁的本质: 并发事务 + 锁 + 不一致的资源获取顺序

  • ✅ 统一加锁顺序
  • ✅ 缩短事务
  • ✅ 合理索引
  • ✅ 控制隔离级别
  • ✅ 应用层重试
  • ✅ 监控 & 日志分析
相关推荐
DCTANT1 分钟前
【原创】使用更优雅的方式改造MyBatisPlus逻辑删除插件
spring boot·后端·mysql·kotlin·mybatis·mybatisplus
鸠摩智首席音效师14 分钟前
MySQL ERROR 1114 (HY000): The table is full
数据库·mysql
数据大魔方18 分钟前
【期货量化实战】豆粕期货量化交易策略(Python完整代码)
开发语言·数据库·python·算法·github·程序员创富
Codeking__38 分钟前
Redis的value类型介绍——zset
数据库·redis·缓存
muddjsv40 分钟前
SQLite3 核心命令全解析 (从入门到精通)
数据库
難釋懷44 分钟前
认识NoSQL
数据库·nosql
亿坊电商1 小时前
利于SEO优化的CMS系统都有哪些特点?
前端·数据库
阿阿阿安1 小时前
MySQL(一)数据库风险操作场景总结
数据库·mysql
计算机程序设计小李同学1 小时前
平价药店销售与管理系统
java·mysql·spring·spring cloud·ssm
心丑姑娘1 小时前
使用ClickHouse时的劣质SQL样例
数据库·sql·clickhouse