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. 循环等待
相关推荐
摇滚侠7 小时前
《On Java 中文版 基础卷》阅读笔记 实现隐藏 12
java·数据库·笔记
川石课堂软件测试8 小时前
学习软件测试需掌握的一些知识
数据库·python·sql·功能测试·网络协议·mysql·http
2501_937767169 小时前
自建 SQL 数据库定时备份踩坑复盘与落地思路
数据库·sql
silvamux_10 小时前
10款模型写种草笔记:可发布性、返工来源与成本数据缺口复盘
java·数据库·笔记
I Am a robert girl12 小时前
从 GameHorizon 拆解多时间尺度游戏评测:数据管线、离线基准与在线回放的三层设计
数据库·游戏·多时间尺度·数据管线·游戏评测·离线基准·在线回放
躺柒12 小时前
读数据安全架构设计与实践17安全架构案例(下)
数据库·安全·零信任·安全架构·防火墙·无边界
SelectDB12 小时前
实时更新排错现场:导入了却查到旧数据,和 FINAL 越加越慢的两条路
大数据·数据库·数据分析
SelectDB13 小时前
拿 Planet 的账单一行行对下来,我总结出三个可自查的校验点
大数据·数据库·数据分析
SelectDB13 小时前
Apache Doris + Vane:让多模态数据进入业务分析
大数据·数据库·数据分析
pjj1985415 小时前
ClickHouse 与 Doris 数据库技术梳理:看懂分析型数据库
数据库·clickhouse