Mysql数据库数据软隔离的并发死锁情况

逻辑隔离表死锁问题分析报告,记录遇到的一个死锁问题。

在多服同表的场景下,采用server_id字段进行数据软隔离,各服访问的数据不存在交叉,但是实际如果索引利用不当,依然可能会引发死锁:

  1. 数据库表和索引,隔离级别为可重复读

    sql 复制代码
    CREATE 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;
  2. 死锁原因(...表示字段展开省略)

    sql 复制代码
    -- 1. 各关键节点服务器负责各自清除一组旧的相关数据(数据隔离互不干扰)
    DELETE FROM `zone` WHERE `server_id` IN (...) AND `mode` = ...;
    sql 复制代码
    -- 2. 随后同节点服务器重新维护一组新的初始数据(数据隔离互不干扰)
    INSERT INTO `zone`(...) VALUES (...) ON DUPLICATE KEY UPDATE ...=...;
  3. 偶发死锁,查询INNODB STATUS日志

    INSERT持有锁:

    RECORD LOCKS space id 2 page no 18 n bits 504 index assigned_zone of table zone trx id 1 lock_mode X

    Record 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 zone trx id 1 lock_mode X locks gap before rec insert intention waiting

    Record lock, heap no 149
    DELETE持有锁:

    RECORD LOCKS space id 2 page no 26 n bits 352 index PRIMARY of table zone trx id 2 lock_mode X

    Record lock, heap no 149
    DELETE等待锁:

    RECORD LOCKS space id 2 page no 18 n bits 504 index assigned_zone of table zone trx id 2 lock_mode X locks rec but not gap waiting

    Record 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 的邻接范围、同一页、同一键前缀范围内,就可能形成等待链
  4. 快速解决方案

    根据业务逻辑,修改 DELETE 语句的 WHERE 条件,改用 assigned_server_id 而非 server_id:先触发 UNIQUE 索引加锁 → 再加 PRIMARY 索引锁。保证锁顺序一致,消除了死锁隐患。

  5. 总结

    5.1 InnoDB加锁原理

    对于有多个UNIQUE索引的INSERT操作,InnoDB的加锁顺序为:

    1. 先加 UNIQUE 索引锁 - 保证唯一性约束

    2. 再加 PRIMARY 索引锁 - 保证主键约束

    对于DELETE操作,加锁顺序取决于WHERE条件使用的索引:

    • 使用 PRIMARY 索引的条件 → 先加 PRIMARY 锁

    • 使用 UNIQUE 索引的条件 → 先加 UNIQUE 锁

    5.2 死锁的本质

    死锁四要素:

    1. 互斥条件
    2. 不可剥夺
    3. 占用且等待
    4. 循环等待
相关推荐
倔强的石头_1 天前
《Kingbase护城河》——数据库存储空间全景探测与精细化瘦身实战
数据库
云技纵横1 天前
唯一索引 INSERT 死锁实战:5 秒复现交叉插入的 S 锁循环等待
sql·mysql
沉默王二1 天前
面试官:RAG 不用向量数据库,用 MySQL 硬扛?我:100 万向量不是很轻松?
mysql·面试·ai编程
冬奇Lab2 天前
每日一个开源项目(第134篇):Zvec - 阿里开源的嵌入式向量数据库,向量搜索界的 SQLite
数据库·人工智能·llm
小猿姐2 天前
MySQL Top 10 热点问题 AI 运维实战:从内核诊断到云原生运维
mysql·云原生·aiops
ClouGence2 天前
Oracle CDC 架构优化:从主库直连到 DataGuard 备库同步
数据库·后端·oracle
云技纵横2 天前
Gap Lock 死锁实战:5 秒在本地复现 MySQL 间隙锁死锁
后端·mysql
无响应de神2 天前
三、用户与权限管理
数据库·mysql
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql