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、总结

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

  • ✅ 统一加锁顺序
  • ✅ 缩短事务
  • ✅ 合理索引
  • ✅ 控制隔离级别
  • ✅ 应用层重试
  • ✅ 监控 & 日志分析
相关推荐
雾时之林9 小时前
Linux--软件管理、源码包安装
linux·服务器·数据库
老纪的技术唠嗑局10 小时前
超级干货分享:中小企业使用 OceanBase 实践经验汇总
数据库
CLOUD ACE11 小时前
谷歌云代理|零售商Target 如何利用 Spanner Graph 提升零售发现体验并将数据库维护成本降低 50%
数据库·零售
AI大模型-小雄11 小时前
Codex写分页接口为什么越翻越慢?用Cursor Pagination解决重复与漏数据
大数据·数据库·elasticsearch·搜索引擎·chatgpt·后端开发·codex
少晓年11 小时前
从 MySQL 迁移到人大金仓 KingbaseES:完整指南与实践
数据库·mysql
SelectDB12 小时前
Apache Doris AI RAG 实战:从基础 RAG 到知识图谱增强的技术能力与选型
数据库
SelectDB12 小时前
电商企业 PostgreSQL 迁移 Apache Doris:80TB 分析数据统一平台技术能力与实践
数据库
SelectDB12 小时前
阶跃星辰 Agent 可观测:Apache Doris / SelectDB 的技术能力与实践
数据库
SelectDB12 小时前
ApacheDoris Iceberg V3 湖仓 DML:Apache Doris / SelectDB 的技术能力与实践
数据库
SelectDB12 小时前
ApacheDoris Python UDF:SQL 调用 Python 的技术能力、选型对比与实践
数据库