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. 循环等待
相关推荐
李兆龙的博客4 小时前
问津集 #26:Lakebase——Postgres 的版本化页面存储、数据库分支与计算弹性
数据库
倔强的石头_6 小时前
聊聊金仓KFS:一款把数据同步软件做扎实的产品
数据库
闲云野鹤在人间6 小时前
MySQL|从理论、安装、备份到主从复制、MHA高可用详解
linux·运维·数据库·mysql·云计算
禾小西6 小时前
Redis:从两大维度和三大主线建立知识体系
数据库·redis·缓存
禾小西7 小时前
Redis 数据结构:快速的 Redis 有哪些慢操作?
数据结构·数据库·redis
数据库小学妹7 小时前
数据共享交换平台选型:交换方式对比与避坑指南
数据库·信创·数据同步·数据交换平台·政务数据共享·数据共享交换平台·数据库底座
要开心吖ZSH8 小时前
MySQL 慢 SQL 排查操作手册-个人笔记版
java·笔记·sql·mysql·慢查询
loong_XL9 小时前
决策模型做内容安全检测:从踩坑到上线
数据库·安全·jev·决策模型
这个DBA有点耶9 小时前
数据库双轨并行实战:全量并行策略、增量延迟控制、双向回切与一致性校验
数据库·架构·dba
Gl�ria9 小时前
MySQL 单机版 vs 高可用版:宕机排查 + 故障处理
mysql·adb·架构