5.6.2 ⾏级锁死锁

InnoDB 行级锁死锁完全指南

行级锁死锁是 InnoDB 高并发场景下最常见的"隐形杀手"。它不像表级锁死锁那样直观,往往隐藏在看似正常的 SQL 背后,因索引、隔离级别和事务顺序的细微差异而突然爆发。理解行级锁死锁的本质,是写出高可靠数据库应用的关键一步。


一、行级锁死锁的本质

行级锁死锁的根本原因与所有死锁相同:两个或多个事务相互持有对方需要的锁,形成循环等待

但在行级锁场景下,这种循环等待更加"精细"和"隐蔽":

  • 锁的粒度是行(索引记录),因此死锁可能发生在仅涉及几行数据的操作之间。
  • 锁的范围可能因间隙锁(Gap Lock) 而扩展,导致原本不冲突的行被锁住,形成意想不到的死锁。
  • 不同事务访问相同行但顺序不同 ,或者访问不同行但有间隙重叠,都可能触发死锁。

二、行级锁死锁的四大经典场景

2.1 场景一:不同事务按相反顺序更新同一组行(最常见的死锁)

表结构

sql 复制代码
CREATE TABLE account (
    id INT PRIMARY KEY,
    balance INT
) ENGINE=InnoDB;
INSERT INTO account VALUES (1, 100), (2, 200);

事务A

sql 复制代码
BEGIN;
UPDATE account SET balance = balance - 10 WHERE id = 1;   -- 持有 id=1 的 X 锁
UPDATE account SET balance = balance + 10 WHERE id = 2;   -- 等待 id=2 的 X 锁
COMMIT;

事务B

sql 复制代码
BEGIN;
UPDATE account SET balance = balance - 20 WHERE id = 2;   -- 持有 id=2 的 X 锁
UPDATE account SET balance = balance + 20 WHERE id = 1;   -- 等待 id=1 的 X 锁
COMMIT;

时序:A 先锁 1,B 先锁 2,然后 A 请求 2(被 B 持有),B 请求 1(被 A 持有)→ 死锁。

解决方案:强制所有事务按相同顺序访问行(如按主键升序)。

2.2 场景二:不同事务锁定重叠范围(RR 级别下的间隙锁死锁)

这是 InnoDB 在 REPEATABLE READ 级别下特有的死锁类型,由间隙锁(Gap Lock)Next-Key Lock 引起。

表结构

sql 复制代码
CREATE TABLE t (id INT PRIMARY KEY, val INT);
INSERT INTO t VALUES (1, 10), (5, 20), (10, 30);

事务A(RR 级别):

sql 复制代码
BEGIN;
-- 锁定 id=5 的 Next-Key Lock,锁住 (1,5] 和 (5,10) 的间隙
SELECT * FROM t WHERE id = 5 FOR UPDATE;

事务B(RR 级别):

sql 复制代码
BEGIN;
-- 试图插入 id=7,属于间隙 (5,10),被事务A的间隙锁阻塞
INSERT INTO t VALUES (7, 25);   -- 等待事务A释放间隙锁

死锁变种:如果事务A和事务B各自锁定不同间隙,然后都尝试插入对方锁定的间隙,就会形成循环等待。

解决方案

  • 降低隔离级别到 READ COMMITTED(无间隙锁)。
  • 或者,在业务层面避免高并发的范围锁定操作。

2.3 场景三:唯一键冲突导致的死锁

当两个事务尝试插入相同唯一键的不同值时,由于唯一索引的检查和锁机制,可能发生死锁。

表结构

sql 复制代码
CREATE TABLE user (id INT PRIMARY KEY, email VARCHAR(50) UNIQUE);

事务A

sql 复制代码
BEGIN;
INSERT INTO user VALUES (1, 'a@x.com');   -- 成功,持有唯一索引上的锁
-- 未提交

事务B

sql 复制代码
BEGIN;
INSERT INTO user VALUES (2, 'a@x.com');   -- 检测到唯一键冲突,等待事务A释放锁

变种死锁:如果事务A和事务B各自先插入不同的唯一值,再试图更新为对方的值,可能形成死锁。

解决方案 :使用 INSERT ... ON DUPLICATE KEY UPDATE 合并插入和更新逻辑,或采用 SELECT ... FOR UPDATE 先锁定再操作。

2.4 场景四:辅助索引与主键索引混合死锁

InnoDB 的行锁既加在聚簇索引上,也加在辅助索引上(辅助索引更新时)。不同事务以不同顺序更新辅助索引和主键,可能导致死锁。

表结构

sql 复制代码
CREATE TABLE orders (id INT PRIMARY KEY, order_no VARCHAR(20) UNIQUE, status INT);
CREATE INDEX idx_status ON orders(status);

事务A

sql 复制代码
BEGIN;
UPDATE orders SET status = 2 WHERE order_no = 'ORD001';  -- 先锁辅助索引(唯一),再锁主键

事务B

sql 复制代码
BEGIN;
UPDATE orders SET status = 1 WHERE id = 100;  -- 先锁主键,再锁辅助索引(status)

如果两个事务同时进行,可能形成循环等待(A 持有唯一索引锁,等待主键;B 持有主键锁,等待辅助索引)。

解决方案 :统一更新路径,优先使用主键更新;或者为所有更新操作加上 SELECT ... FOR UPDATE 锁定主键行,避免锁顺序颠倒。


三、行级锁死锁的底层原理:锁模式与锁类型

要深入理解死锁,必须知道 InnoDB 的行锁类型及其组合。

锁类型 说明 何时产生
行锁(Record Lock) 锁定索引树上的某个叶子节点 基于唯一索引等值查询命中记录
间隙锁(Gap Lock) 锁定索引记录之间的间隙(不包含记录本身) RR 级别下范围查询、插入前检查
Next-Key Lock 记录锁 + 间隙锁(左开右闭区间) RR 级别下默认的锁算法
插入意向锁(Insert Intention Lock) 一种特殊的间隙锁,由 INSERT 在等待间隙锁时设置 INSERT 操作发现间隙已被其他事务锁定

死锁往往由 Next-Key Lock 和插入意向锁的冲突引发。例如,事务A持有间隙锁,事务B持有另一个间隙锁,双方都尝试插入对方间隙,导致循环等待。


四、行级锁死锁的排查实战

4.1 获取死锁日志

sql 复制代码
-- 查看最近一次死锁
SHOW ENGINE INNODB STATUS\G
-- 在输出中查找 "LATEST DETECTED DEADLOCK" 部分

关键信息解读

  • TRANSACTION 块:展示事务 ID、活跃状态、持有的锁和等待的锁。
  • HOLDS THE LOCK(S):显示该事务当前成功获得的锁(通常是 X 锁或 Next-Key Lock)。
  • WAITING FOR THIS LOCK:显示该事务被阻塞的锁请求。
  • WE ROLL BACK TRANSACTION:InnoDB 选择回滚的事务。

4.2 使用 Performance Schema 实时监控(MySQL 8.0)

sql 复制代码
-- 查看当前所有锁
SELECT * FROM performance_schema.data_locks\G;

-- 查看锁等待关系(谁在等谁)
SELECT * FROM performance_schema.data_lock_waits\G;

这些表比 SHOW ENGINE INNODB STATUS 更细致,可以实时查看锁类型、锁模式、持有事务等。

4.3 分析死锁日志示例

日志片段

复制代码
------------------------
LATEST DETECTED DEADLOCK
------------------------
2026-08-14 10:23:45 0x7f8a9c0
*** (1) TRANSACTION:
TRANSACTION 3100, ACTIVE 12 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 8, OS thread handle 12345, query id 100 localhost root updating
UPDATE account SET balance = balance - 10 WHERE id = 2
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 119 page no 3 n bits 72 index PRIMARY of table `test`.`account` trx id 3100 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;  (id=1)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 119 page no 3 n bits 72 index PRIMARY of table `test`.`account` trx id 3100 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000002; asc     ;;  (id=2)

*** (2) TRANSACTION:
TRANSACTION 3101, ACTIVE 10 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 9, OS thread handle 12346, query id 101 localhost root updating
UPDATE account SET balance = balance + 10 WHERE id = 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 119 page no 3 n bits 72 index PRIMARY of table `test`.`account` trx id 3101 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000002; asc     ;;  (id=2)
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 119 page no 3 n bits 72 index PRIMARY of table `test`.`account` trx id 3101 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;  (id=1)
*** WE ROLL BACK TRANSACTION (1)

分析

  • 事务1 (3100) 持有 id=1 的锁,等待 id=2 的锁。
  • 事务2 (3101) 持有 id=2 的锁,等待 id=1 的锁。
  • 循环等待 → 死锁,InnoDB 回滚了事务1(修改行数较少)。

五、行级锁死锁的预防与规避

5.1 统一访问顺序(最重要)

所有事务按照相同的顺序访问表和行。例如,总是先更新主键较小的行,或按字母顺序处理记录。

5.2 使用合适的索引

  • 确保 UPDATE/DELETEWHERE 条件命中索引,避免全表扫描导致的锁升级。
  • 尽量使用唯一索引主键进行更新,减少间隙锁的范围。

5.3 降低隔离级别

如果业务允许,将隔离级别从 REPEATABLE READ 降为 READ COMMITTED,可以消除间隙锁,大幅降低死锁概率。但需接受不可重复读和幻读(在 RC 下)。

5.4 缩短事务

  • 将大事务拆分为多个小事务,尽早提交释放锁。
  • 避免在事务中进行耗时的外部调用(如 RPC、文件操作)。

5.5 使用 FOR UPDATE 提前锁定

对于需要先查询再更新的业务,使用 SELECT ... FOR UPDATE 在查询阶段就锁定目标行,避免在更新阶段发生锁顺序颠倒。

5.6 使用 INSERT ... ON DUPLICATE KEY UPDATE

合并插入和更新逻辑,减少锁请求次数。

5.7 合理设置锁等待超时

innodb_lock_wait_timeout 默认 50 秒,可适当调小(如 10 秒),避免长时间等待导致连接堆积。但请注意,超时回滚也会造成业务失败,需配合重试。

5.8 监控长事务与死锁频次

  • 定期查询 information_schema.innodb_trx,发现超过数秒的事务需告警。
  • 开启 innodb_print_all_deadlocks = ON,将所有死锁记录到错误日志,便于事后分析。

六、应用层应对策略:重试机制

死锁是并发系统的常态,无法完全避免。 因此,应用层必须设计优雅的重试机制。

伪代码模板

python 复制代码
max_retries = 3
retry_count = 0
while retry_count < max_retries:
    try:
        with db.transaction():
            # 执行事务操作
            do_update()
        break  # 成功则退出
    except DeadlockError as e:
        retry_count += 1
        if retry_count >= max_retries:
            raise  # 重试次数用尽,向上抛出
        # 等待随机时间(避免活锁)
        sleep(random.uniform(0.01, 0.1))

关键 :重试是处理死锁唯一可靠的方式。不要试图通过调整锁顺序完全消除死锁,那在复杂系统中几乎不可能。


七、总结:行级锁死锁速查表

死锁类型 典型场景 最有效对策
按相反顺序更新 事务A更新1→2,事务B更新2→1 统一访问顺序(按主键升序)
间隙锁死锁 RR 下范围查询 + INSERT 插入间隙 降级为 RC 隔离级别
唯一键冲突死锁 并发插入相同唯一键的不同业务 使用 INSERT ... ON DUPLICATE KEY UPDATE
辅助索引混锁 不同索引访问顺序不同 统一使用主键作为更新条件
长事务锁堆积 大事务持有大量锁,小事务等待 拆分大事务,及时提交

一句话记住行级锁死锁

行级锁死锁是 InnoDB 并发性能的"隐形天花板"。要突破它,核心是:统一访问顺序、降低隔离级别(如可行)、缩短事务、并为 SQL 建立精准索引。最终,应用层必须拥抱重试,把死锁当作正常的并发事件来优雅处理。

相关推荐
ChaHae-In1 小时前
MyBatis动态SQL与MyBatis-Plus高效开发指南
数据库·oracle·mybatis
冰暮流星2 小时前
mysql之分组查询
数据库·mysql
倒流时光三十年2 小时前
PostgreSQL Semi-Join(半连接)通俗讲解
数据库·postgresql
NiceCloud喜云3 小时前
腾讯云国际版云数据库选型:MySQL、Redis 怎么按业务架构来判断
数据库·mysql·腾讯云
ew452183 小时前
MySQL JOIN 关联语法、差异、标准用法、经典坑点汇总
数据库·mysql
Rain的Java大神之路4 小时前
线上接口负载满了如何解决
java·数据库·redis·后端·缓存·面试·架构
Nturmoils4 小时前
给运营导个 CSV,在 ksql 里用 \copy 就够了
数据库
ciqingloveless4 小时前
Oracle打开数据库加密
数据库·oracle
m0_462605224 小时前
大模型week02
数据库