逻辑隔离表死锁问题分析报告,记录遇到的一个死锁问题。
在多服同表的场景下,采用server_id字段进行数据软隔离,各服访问的数据不存在交叉,但是实际如果索引利用不当,依然可能会引发死锁:
-
数据库表和索引,隔离级别为可重复读
sqlCREATE TABLE `zone` ( `server_id` int NOT NULL COMMENT '服务器', `guild_id` int NOT NULL COMMENT '公会,隶属于server_id', `assigned_server_id` int NOT NULL DEFAULT '0' COMMENT '分配服务器', `mode` int NOT NULL DEFAULT '0' COMMENT '模式', `zone_id` int NOT NULL DEFAULT '0' COMMENT '战区', ..., PRIMARY KEY (`server_id`,`guild_id`,`mode`), UNIQUE KEY `assigned_zone` (`assigned_server_id`,`mode`,`zone_id`) ) ENGINE=InnoDB DEFAULT; -
死锁原因(
...表示字段展开省略)sql-- 1. 各关键节点服务器负责各自清除一组旧的相关数据(数据隔离互不干扰) DELETE FROM `zone` WHERE `server_id` IN (...) AND `mode` = ...;sql-- 2. 随后同节点服务器重新维护一组新的初始数据(数据隔离互不干扰) INSERT INTO `zone`(...) VALUES (...) ON DUPLICATE KEY UPDATE ...=...; -
偶发死锁,查询
INNODB STATUS日志INSERT持有锁:RECORD LOCKS space id 2 page no 18 n bits 504 index assigned_zone of table
zonetrx id 1 lock_mode XRecord lock, heap no 23 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
Record lock, heap no 225 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
INSERT等待锁:RECORD LOCKS space id 2 page no 26 n bits 352 index PRIMARY of table
zonetrx id 1 lock_mode X locks gap before rec insert intention waitingRecord lock, heap no 149
DELETE持有锁:RECORD LOCKS space id 2 page no 26 n bits 352 index PRIMARY of table
zonetrx id 2 lock_mode XRecord lock, heap no 149
DELETE等待锁:RECORD LOCKS space id 2 page no 18 n bits 504 index assigned_zone of table
zonetrx id 2 lock_mode X locks rec but not gap waitingRecord lock, heap no 23 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
3.1 加锁时序分析
阶段 INSERT DELETE 第一步 在 assigned_zone索引加X锁在 PRIMARY索引加X锁第二步 等待 PRIMARY索引的间隙锁等待 assigned_zone索引的 X 锁结果 死锁 死锁 3.2 根本原因
两个事务的加锁顺序相反:
-
INSERT操作:-
先加锁 assigned_zone 索引 ← 由于有 UNIQUE 约束需要检查唯一性
-
再加锁 PRIMARY 索引
-
-
DELETE操作:-
先加锁 PRIMARY 索引 ← WHERE 条件基于 server_id (PRIMARY 的一部分)
-
再加锁 assigned_zone 索引 ← 需要检查对应的 UNIQUE 索引记录
-
3.3 死锁发生的完整过程
Transaction 1 (INSERT): ① 获得 assigned_zone 唯一索引上的锁(两条记录) ② 需要等待 PRIMARY 索引上的间隙锁 Transaction 2 (DELETE): ① 获得 PRIMARY 索引上的锁(一条记录) ② 需要等待 assigned_zone 索引上的锁 => 循环等待,死锁! 即便两批业务数据"理论不交叉",只要它们在同一棵 B+Tree 的邻接范围、同一页、同一键前缀范围内,就可能形成等待链 -
-
快速解决方案
根据业务逻辑,修改
DELETE语句的WHERE条件,改用assigned_server_id而非server_id:先触发 UNIQUE 索引加锁 → 再加 PRIMARY 索引锁。保证锁顺序一致,消除了死锁隐患。 -
总结
5.1 InnoDB加锁原理
对于有多个UNIQUE索引的INSERT操作,InnoDB的加锁顺序为:
-
先加 UNIQUE 索引锁 - 保证唯一性约束
-
再加 PRIMARY 索引锁 - 保证主键约束
对于DELETE操作,加锁顺序取决于WHERE条件使用的索引:
-
使用 PRIMARY 索引的条件 → 先加 PRIMARY 锁
-
使用 UNIQUE 索引的条件 → 先加 UNIQUE 锁
5.2 死锁的本质
死锁四要素:
- 互斥条件
- 不可剥夺
- 占用且等待
- 循环等待
-