乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 数据库基础与安装

    • MySQL 简介
    • MySQL 5.6 到 5.7 到 8.0 关键差异速查
    • 安装 MySQL 5.7
    • 连接与断开服务器
    • 创建数据库
    • 创建数据表
    • 数据库与数据表
    • 加载数据
    • 获取数据库信息
    • 批处理模式
    • SHOW 语句汇总
    • FLUSH 与 RESET 语句
    • my.cnf 核心参数
    • 字符集与排序规则
  • 第2章 SQL基础查询

    • SELECT
    • WHERE
    • ORDER BY
    • LIMIT
    • COUNT
    • 聚合函数
    • 比较运算符
    • 逻辑运算符
    • 算术运算符
    • 模式匹配
    • NULL 值处理
    • UPDATE
    • DELETE
    • REPLACE
    • SELECT INTO
  • 第3章 数据类型与运算符

    • 数值类型
    • 字符串类型
    • 日期时间类型
    • BIT 类型
    • ENUM 类型
    • SET 类型
    • JSON 类型
    • 类型转换
  • 第4章 函数与表达式

    • 字符串函数
    • 数值函数
    • 日期函数
    • 全文检索函数
  • 第5章 高级查询与子查询

    • JOIN
    • 子查询
    • UNION
    • GROUP BY
    • HAVING
    • DISTINCT
  • 第6章 表与索引

    • 数据定义语言
    • 修改表结构
    • 视图
    • 修改视图与检查选项
    • 外键
    • 索引
    • 唯一索引
    • 复合索引
    • 存储引擎对比
    • 分区表
    • 第一范式与第二范式
    • 第三范式与 BC 范式
    • 反范式设计
  • 第7章 存储过程与函数

    • 存储过程
    • 存储函数
    • 变量
    • 流程控制
    • 游标
    • 预处理语句
  • 第8章 事务与锁

    • 事务
    • 事务隔离级别
    • 锁机制
    • MVCC
    • 死锁专题分析
    • LOCK TABLES
    • XA 事务
  • 第9章 用户管理与安全

    • 用户管理
    • 权限管理
    • 角色
    • SQL 注入防范
  • 第10章 性能优化入门

    • 执行计划
    • 索引优化
    • 查询优化
    • 查询优化器提示
    • 慢查询日志
    • InnoDB 深入机制
    • InnoDB 专项优化
    • Performance Schema
    • sys Schema
  • 第11章 复制与高可用

    • 主从复制原理
    • 半同步复制配置
    • binlog 开启与 point-in-time 恢复
    • mysqldump 全库备份
    • mysqldump 单表与条件备份
    • mysqldump 恢复与导入
    • xtrabackup 全量热备
    • xtrabackup 准备与恢复
    • xtrabackup 增量与流式备份
  • 第12章 触发器与事件

    • 触发器
    • 事件调度器
  • 参考

    • MySQL 5.7 专业术语大全
    • MySQL 5.7 关键字与保留字大全

死锁专题分析

导学

事务和锁保证了并发安全,但两把锁交叉等待时就会陷入死锁——事务 A 持有锁 1 等待锁 2,事务 B 持有锁 2 等待锁 1,双方永远等下去。MySQL 5.7 有自动死锁检测,但检测本身也有开销。本节从死锁的四个必要条件出发,教你读懂 SHOW ENGINE INNODB STATUS 的死锁日志,并写出最小化死锁风险的 SQL。

定义

死锁(Deadlock):两个或多个事务互相持有对方需要的锁,且都在等待对方释放,导致所有涉及事务都无法继续执行的状态。InnoDB 会自动检测死锁并选择一个事务作为"牺牲者"回滚,但频繁死锁会严重拖累性能。

死锁的四个必要条件

死锁同时满足以下四个条件才会发生,破坏任意一个即可避免:

条件含义在 MySQL 中的体现破坏手段
互斥资源一次只能被一个事务占用行锁(X锁/S锁)是互斥的无法破坏(锁的本质)
持有并等待事务持有锁的同时还在请求新锁事务 A 持有记录 1 的锁,等待记录 2一次性申请所有需要的锁;或按固定顺序加锁
不可抢占已获得的锁不能被强制剥夺InnoDB 不支持锁抢占,只能等事务提交/回滚设置锁超时(innodb_lock_wait_timeout)
循环等待事务之间形成环形等待链A 等 B,B 等 A按主键/索引固定顺序访问记录

场景一:构造一个死锁

当前数据状态:

CREATE TABLE employees (
    emp_id INT PRIMARY KEY,
    emp_name VARCHAR(20),
    score DECIMAL(5,2)
) ENGINE=InnoDB;

INSERT INTO employees VALUES (1, '大翔', 100), (2, '白歌', 85);
emp_idemp_namescore
1大翔100
2白歌85

操作语句(两个会话同时执行):

会话 A:

START TRANSACTION;
UPDATE employees SET score = score + 10 WHERE emp_id = 1;  -- 成功,持有 emp_id=1 的 X 锁
-- 此时切换到会话 B 执行第一步
UPDATE employees SET score = score + 10 WHERE emp_id = 2;  -- 等待会话 B 释放 emp_id=2 的 X 锁
-- 死锁发生,MySQL 自动回滚本事务

会话 B:

START TRANSACTION;
UPDATE employees SET score = score + 10 WHERE emp_id = 2;  -- 成功,持有 emp_id=2 的 X 锁
-- 此时切换到会话 A 执行第二步
UPDATE employees SET score = score + 10 WHERE emp_id = 1;  -- 等待会话 A 释放 emp_id=1 的 X 锁
-- 死锁发生,MySQL 自动回滚本事务(或会话 A 被回滚)

结果解读:

  • 会话 A 持有记录 1 的锁,等待记录 2
  • 会话 B 持有记录 2 的锁,等待记录 1
  • 形成循环等待,死锁产生
  • InnoDB 死锁检测器发现后,选择代价较小的事务(通常修改行数少、undo 量小)作为牺牲者回滚,并返回错误:ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
  • 未被回滚的事务继续执行,成功提交

场景二:查看死锁日志

死锁发生后,立即执行:

SHOW ENGINE INNODB STATUS;

在输出中找到 LATEST DETECTED DEADLOCK 段落(节选):

------------------------
LATEST DETECTED DEADLOCK
------------------------
2024-01-15 14:32:10 0x7f8b8c0a2700
*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 12 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 15, OS thread handle 123456789, query id 100 localhost root updating
UPDATE employees SET score = score + 10 WHERE emp_id = 2
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY of table `company`.`employees` trx id 12345 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000001; asc     ;;  -- emp_id = 1
 1: len 6; hex 000000000303; asc      ;;
 2: len 7; hex b20000011b0110; asc        ;;
 3: len 6; hex e5a4a7e7bf 94; asc 大翔;;  -- emp_name = 大翔

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY of table `company`.`employees` trx id 12345 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000002; asc     ;;  -- emp_id = 2
 ...

*** (2) TRANSACTION:
TRANSACTION 12346, ACTIVE 10 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 16, OS thread handle 123456790, query id 101 localhost root updating
UPDATE employees SET score = score + 10 WHERE emp_id = 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY of table `company`.`employees` trx id 12346 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000002; asc     ;;  -- emp_id = 2
 ...

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY of table `company`.`employees` trx id 12346 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000001; asc     ;;  -- emp_id = 1
 ...

*** WE ROLL BACK TRANSACTION (1)

日志解读:

字段含义
TRANSACTION 12345事务 ID,唯一标识
ACTIVE 12 sec事务已活跃 12 秒
HOLDS THE LOCK(S)该事务当前持有的锁
WAITING FOR THIS LOCK该事务正在等待的锁
lock_mode X排他锁(X Lock)
locks rec but not gap记录锁,非间隙锁
hex 00000001十六进制表示的主键值 1(大翔)
WE ROLL BACK TRANSACTION (1)事务 1(12345)被选为牺牲者回滚

结果解读:

  • 事务 1(会话 A)持有 emp_id=1(大翔)的 X 锁,等待 emp_id=2(白歌)的 X 锁
  • 事务 2(会话 B)持有 emp_id=2(白歌)的 X 锁,等待 emp_id=1(大翔)的 X 锁
  • MySQL 选择回滚事务 1,事务 2 继续执行

场景三:死锁检测机制

innodb_deadlock_detect:控制是否开启死锁检测,默认 ON。

-- 查看当前设置
SELECT @@innodb_deadlock_detect;
@@innodb_deadlock_detect
1

操作语句:关闭死锁检测后,死锁不再被自动检测,事务将一直等待直到超时。

SET GLOBAL innodb_deadlock_detect = OFF;

结果解读:

  • ON(默认):InnoDB 主动检测死锁,发现后立即回滚牺牲者事务。检测通过**等待图(Wait-For Graph)**实现——每个事务是节点,等待关系是边,检测图中是否存在环。
  • OFF:不主动检测死锁。如果发生死锁,事务会一直等待,直到 innodb_lock_wait_timeout(默认 50 秒)超时后报错 Lock wait timeout exceeded。
  • 高并发场景下死锁检测是性能瓶颈:当大量事务(> 1000)并发更新同一批热点行时,死锁检测需要遍历等待链,CPU 飙升。此时可考虑关闭死锁检测,改用 innodb_lock_wait_timeout 控制,但会牺牲响应速度。

场景四:间隙锁导致的死锁

当前数据状态:

CREATE TABLE employees_gap (
    emp_id INT PRIMARY KEY,
    emp_name VARCHAR(20),
    score DECIMAL(5,2)
) ENGINE=InnoDB;

INSERT INTO employees_gap VALUES (1, '大翔', 100), (5, '白歌', 85), (10, '孔蓝', 90);
emp_idemp_namescore
1大翔100
5白歌85
10孔蓝90

操作语句(RR 隔离级别下):

会话 A:

START TRANSACTION;
-- 范围查询加间隙锁,锁住 (1, 5] 和 (5, 10] 的间隙
SELECT * FROM employees_gap WHERE emp_id > 1 AND emp_id < 10 FOR UPDATE;
-- 此时切换到会话 B
INSERT INTO employees_gap VALUES (7, '小崔', 80);  -- 等待会话 B 释放间隙锁

会话 B:

START TRANSACTION;
-- 同样范围查询,请求间隙锁
SELECT * FROM employees_gap WHERE emp_id > 1 AND emp_id < 10 FOR UPDATE;
-- 间隙锁不冲突,成功(双方都是间隙锁,兼容)
-- 但接下来
INSERT INTO employees_gap VALUES (8, '小赵', 75);  -- 等待会话 A 释放间隙锁

结果解读:

  • 在 REPEATABLE READ 下,FOR UPDATE 的范围查询会加间隙锁(Gap Lock)
  • 双方先获得各自的间隙锁(间隙锁之间不互斥),然后都尝试在对方锁住的间隙内插入新记录
  • 插入意向锁(Insert Intention Lock)与间隙锁冲突,形成死锁
  • 这种死锁比行锁死锁更隐蔽,因为双方第一步都"成功"了

最小化死锁的 SQL 编写策略

策略一:按固定顺序访问记录

问题根源:交叉访问顺序是死锁最常见的原因。

正确写法:

-- 无论业务逻辑如何,UPDATE 多条记录时始终按主键升序
START TRANSACTION;
UPDATE employees SET score = score + 10 WHERE emp_id = 1;  -- 先访问小 ID
UPDATE employees SET score = score + 10 WHERE emp_id = 2;  -- 再访问大 ID
COMMIT;

结果解读:如果所有事务都按 emp_id 升序加锁,就不会出现 A 先锁 1 再锁 2、B 先锁 2 再锁 1 的交叉情况。循环等待条件被破坏。

策略二:一次性申请所有锁(减少持有并等待)

-- 不好:先查再逐条更新,每条都单独加锁
START TRANSACTION;
SELECT emp_id FROM employees WHERE dept = '技术部';  -- 查出 1, 2
UPDATE employees SET score = score + 10 WHERE emp_id = 1;
UPDATE employees SET score = score + 10 WHERE emp_id = 2;
COMMIT;

-- 更好:用 IN 一次性更新,MySQL 内部会排序后统一加锁
START TRANSACTION;
UPDATE employees SET score = score + 10 WHERE emp_id IN (1, 2);
COMMIT;

结果解读:UPDATE ... WHERE emp_id IN (1, 2) 比两条独立的 UPDATE 更安全,因为 InnoDB 会一次性确定所有需要加锁的行,并按顺序加锁,减少中间状态被其他事务插入的机会。

策略三:缩短事务长度

-- 不好:事务中夹杂业务逻辑、网络请求、用户交互
START TRANSACTION;
UPDATE employees SET score = 100 WHERE emp_id = 1;
-- 这里调用外部 API,耗时 3 秒
UPDATE employees SET score = 90 WHERE emp_id = 2;
COMMIT;

-- 更好:事务只包含纯数据库操作
START TRANSACTION;
UPDATE employees SET score = 100 WHERE emp_id = 1;
UPDATE employees SET score = 90 WHERE emp_id = 2;
COMMIT;
-- 外部 API 在事务外调用

结果解读:事务持有锁的时间越长,与其他事务冲突的概率越大。将非数据库操作移到事务外,是降低死锁率最有效的手段之一。

策略四:降低隔离级别(权衡一致性)

-- 如果业务允许不可重复读和幻读,使用 READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT * FROM employees WHERE emp_id = 1 FOR UPDATE;
COMMIT;

结果解读:READ COMMITTED 下 InnoDB 不使用间隙锁(Gap Lock),只加记录锁。这消除了大量由间隙锁导致的死锁,但会引入幻读问题。适合对一致性要求不极端、并发量大的场景。

策略五:设置合理的锁等待超时

-- 查看当前超时设置
SELECT @@innodb_lock_wait_timeout;
@@innodb_lock_wait_timeout
50
-- 业务场景允许时,适当缩短超时,避免死锁检测关闭时长等待
SET SESSION innodb_lock_wait_timeout = 10;

结果解读:innodb_lock_wait_timeout 控制事务等待锁的最长时间(秒)。如果关闭死锁检测(innodb_deadlock_detect = OFF),这个参数是唯一的"逃生通道"。即使开启死锁检测,缩短超时也能让非死锁的单纯锁等待更快失败,减少连接堆积。

常见误区

误区正解
"死锁是 MySQL 的 bug"不是。死锁是并发事务正常交互的结果,任何支持事务的数据库都可能发生。
"死锁检测关闭后就不会死锁"不是。死锁仍然会发生,只是 MySQL 不再自动检测和回滚,事务会等到超时。
"死锁只发生在 UPDATE 上"不是。SELECT ... FOR UPDATE、DELETE、INSERT(间隙锁冲突)都可能死锁。
"牺牲者事务一定是后启动的"不是。MySQL 选择牺牲者的依据是 undo 量(回滚代价),通常是修改行数少、事务轻的那个,与启动顺序无关。
"死锁日志里的 hex 值看不懂"hex 00000001 就是主键值 1 的 4 字节大端表示。可以用 SELECT CONV('00000001', 16, 10) 验证。

面试考点

Q:死锁的四个必要条件?

互斥、持有并等待、不可抢占、循环等待。在 MySQL 中,破坏循环等待最有效的方式是按固定顺序访问记录(如按主键升序);破坏持有并等待的方式是一次性申请所有锁或缩短事务。

Q:SHOW ENGINE INNODB STATUS 死锁日志怎么看?

找 LATEST DETECTED DEADLOCK 段落。看 (1) HOLDS THE LOCK(S) 和 (1) WAITING FOR THIS LOCK 分别表示事务 1 持有的锁和等待的锁;(2) 同理。WE ROLL BACK TRANSACTION (N) 表示回滚了哪个事务。lock_mode X 是排他锁,S 是共享锁,locks rec but not gap 是记录锁,locks gap before rec 是间隙锁。

Q:高并发下死锁检测为什么消耗 CPU?

死锁检测通过遍历等待图(Wait-For Graph)找环实现。当大量事务并发访问同一批热点行时,等待关系复杂,检测算法的时间复杂度上升,导致 CPU 飙升。此时可考虑关闭 innodb_deadlock_detect,依赖 innodb_lock_wait_timeout,但会牺牲响应速度。

Q:间隙锁死锁和普通行锁死锁有什么区别?

行锁死锁通常是双方已持有不同行的 X 锁,再请求对方持有的行。间隙锁死锁更隐蔽:双方先获得兼容的间隙锁(不互斥),然后都试图在对方锁住的间隙内插入记录,插入意向锁与间隙锁冲突,形成死锁。降低隔离级别到 READ COMMITTED 可消除间隙锁死锁。

小结

  • 死锁四条件:互斥、持有并等待、不可抢占、循环等待。破坏循环等待和持有并等待是 SQL 层最有效的手段
  • 按固定顺序(如主键升序)访问记录,可彻底消除交叉等待型死锁
  • SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK 段落是分析死锁的唯一可靠来源
  • 缩短事务长度、一次性申请所有锁、适当降低隔离级别,都是降低死锁率的有效策略
  • 高并发热点行场景下,innodb_deadlock_detect 可能成为 CPU 瓶颈,需权衡检测与超时策略

清理测试数据:

DROP TABLE IF EXISTS employees;
DROP TABLE IF EXISTS employees_gap;

下一章引子:死锁是并发控制的极端情况,而日常开发中更常见的是锁等待导致的慢查询。Performance Schema 和 sys Schema 提供了系统化的锁等待诊断工具。

上一页
MVCC
下一页
LOCK TABLES