大数据技术

MySQL:死锁日志里只看到两个事务各持有锁又互相等待,怎么快速判断是加锁顺序问题、索引缺失还是Gap Lock造成的?

👤 为我痴狂 👁 21 阅读 ❤ 0 点赞 ➦ 0 分享 📅 2026-11-02
首页› 大数据› 大数据技术› 正文
MySQL:死锁日志里只看到两个事务各持有锁又互相等待,怎么快速判断是加锁顺序问题、索引缺失还是Gap Lock造成的?

从"两条事务互等"的表象,到"锁序—索引—间隙"三层归因主线:一套可复现、可验证、可落地的死锁定性方法论

摘要

InnoDB 死锁日志的 LATEST DETECTED DEADLOCK 段落通常只呈现"事务 A 持有 X 锁等待 Y 锁、事务 B 持有 Y 锁等待 X 锁"的对称结构。这种对称性本身不携带归因信息,真正决定成因的是锁对象落在哪条索引记录上、该记录是唯一等值命中还是范围扫描产物、以及锁模式是 RECORD 还是 GAP/next-key。本文提出一条贯穿全文的分析主线:先判锁序、再判索引、最后判间隙,把三类高频成因映射为日志中可观测的差异化特征,并给出从日志解析、索引执行计划取证到间隙边界复现的完整操作路径。文中同时讨论 RC 与 RR 隔离级别下的行为差异、唯一索引等值查询的锁退化条件,以及云原生与分布式数据库对传统死锁诊断范式的冲击。

本文评述:现有公开资料多以"死锁案例集"形式罗列现象,缺少统一的归因决策树;笔者认为,把死锁诊断从"看日志猜原因"升级为"按锁对象反推执行路径",才是工程上可复用、可自动化的方向。

一、问题的本质:对称死锁日志为何"信息量很低"

1.1 死锁日志的结构与它"说了什么、没说什么"

执行 SHOW ENGINE INNODB STATUS 得到的 LATEST DETECTED DEADLOCK 段落,典型结构包含三部分:事务 1 的 HOLDS 与 WAITING 锁列表、事务 2 的对应列表、以及 InnoDB 给出的 WE ROLL BACK 裁决。日志中的锁描述形如 lock_mode X locks rec but not gap waiting,它同时编码了锁模式(X/S)、锁粒度(rec/gap/next-key)、等待状态三类信息。问题在于,日志只给出锁的"最终形态",不给出"为什么加这把锁"——即不告诉你这条记录是通过唯一索引等值定位的,还是通过二级索引范围扫描命中的。

这就导致一个常见误判:看到两个事务互相等待,就笼统归因为"并发写冲突",然后简单加锁或重试。实际上,同样是对称结构,成因可能完全不同:可能是应用层两条 SQL 的访问顺序相反,可能是某条 UPDATE 走了全表扫描导致锁范围爆炸,也可能是 RR 隔离级别下范围条件触发了间隙锁。三者的修复手段差异极大——前者改代码顺序,中者加索引,后者调隔离级别或改写条件。若不做分层归因,修复往往"治标不治本",死锁换个时间点复现。

1.2 本文的分析主线:锁序 → 索引 → 间隙

笔者认为,死锁归因应遵循一条由"应用语义"到"存储引擎语义"的递进主线。第一层看锁序:两个事务的加锁对象集合是否存在交叉,且交叉是否源于业务代码中固定的访问顺序差异。第二层看索引:若锁对象数量异常多、或锁落在非预期的记录上,说明执行计划未走预期索引,锁范围被放大。第三层看间隙:若锁模式中出现 gap 或 next-key,且隔离级别为 RR,则需判断是否为范围条件或唯一性检查失败导致的间隙锁。

这条主线的价值在于:它把"日志里看到了什么"与"应该去查什么"绑定起来。每一层都有对应的可观测证据——锁序层看 SQL 文本与参数顺序,索引层看 EXPLAIN 与索引统计,间隙层看隔离级别、索引唯一性与条件边界。三者不是互斥的,很多真实死锁是"索引缺失放大锁范围 + 间隙锁补刀"的复合形态,但归因必须按序排除,否则容易在错误的层次上做无效优化。

本文评述:把死锁诊断类比为"故障树分析"是合适的。锁序是顶层事件(应用行为),索引与间隙是底层事件(引擎行为)。工程上应先确认顶层事件是否成立,再下钻底层,否则容易把引擎的正常加锁行为误判为缺陷。

1.3 一个必须先澄清的前提:死锁是检测结果,不是错误

InnoDB 使用等待图(wait-for graph)做死锁检测,检测到环后主动回滚代价较小的事务,这是正常且必要的机制,而非系统故障。因此诊断目标不是"消灭所有死锁",而是判断某类死锁是否属于可接受的偶发、还是暴露了设计缺陷的高频问题。业界普遍以"单位时间死锁次数"和"是否集中在少数 SQL 模板"作为分级依据,这一实践在多家云厂商的数据库可观测性文档中均有体现(如阿里云 RDS、腾讯云 TDSQL 的死锁分析功能说明)。本文评述:分级意识很重要,把所有死锁都当 P0 处理会导致过度加锁,反而降低并发度。

二、第一层归因:加锁顺序问题(Lock Ordering)

2.1 判定特征:锁对象相同、顺序相反

加锁顺序问题的典型特征是:两个事务操作的资源集合高度重叠,但访问次序相反。在日志中表现为事务 1 持有记录 R1 的锁、等待 R2;事务 2 持有 R2 的锁、等待 R1。这里的 R1、R2 通常是主键或唯一索引上的不同行。若两条事务的 SQL 模板分别形如 UPDATE t SET ... WHERE id=1 与 UPDATE t SET ... WHERE id=2 交替执行,且业务层存在"先 A 后 B"与"先 B 后 A"两条路径,即可高度怀疑锁序问题。

需要强调一个边界条件:只有当锁对象是唯一索引等值命中时,锁序问题才是"干净"的。若锁对象来自范围扫描,日志中会同时出现多条记录锁甚至间隙锁,此时锁序只是表象,根因可能已滑向索引层。因此判锁序前,应先确认日志中每个事务的锁列表长度是否与 SQL 预期影响行数一致。

2.2 复现与验证方法

验证锁序问题最直接的方式是构造最小复现:开启两个会话,按相反顺序执行等值 UPDATE,观察是否稳定复现死锁。以下为可复现的会话脚本(表结构为 t(id INT PRIMARY KEY, v INT),隔离级别 RR):

-- 会话 A
BEGIN;
UPDATE t SET v = v + 1 WHERE id = 1;   -- 持有 id=1 的 X 记录锁
-- 会话 B
BEGIN;
UPDATE t SET v = v + 1 WHERE id = 2;   -- 持有 id=2 的 X 记录锁
-- 会话 A
UPDATE t SET v = v + 1 WHERE id = 2;   -- 等待 B
-- 会话 B
UPDATE t SET v = v + 1 WHERE id = 1;   -- 等待 A,形成环,InnoDB 回滚其一

若上述脚本稳定复现,且日志中每个事务恰好各持一把、各等一把记录锁,则锁序归因成立。修复路径有三:其一,在应用层统一资源访问顺序(如按主键升序批量更新);其二,将多行更新合并为单条 UPDATE ... WHERE id IN (...),由引擎内部按索引顺序加锁;其三,对强冲突场景引入乐观锁版本号或分布式锁串行化。本文评述:方案二在多数场景下性价比最高,但需注意 IN 列表过长时可能触发索引选择变化,反而引入新的锁范围问题。

2.3 为什么锁序问题在微服务架构下更常见

单体应用中,资源访问顺序通常由同一段代码控制,较易统一。微服务拆分后,同一批数据可能被订单服务、库存服务、账务服务分别更新,各服务独立演进,访问顺序缺乏全局约束。这类"跨服务锁序不一致"在日志上与单机锁序问题无法区分,但修复成本高得多——需要引入领域事件排序、Saga 补偿或统一的数据访问层。笔者认为,这是死锁治理从"数据库问题"上升为"架构问题"的典型场景,单纯在 DB 侧加索引无法根治。

三、第二层归因:索引缺失与执行路径退化

3.1 判定特征:锁对象数量远超预期

索引缺失的核心表征是锁范围放大。当 UPDATE/DELETE 的 WHERE 条件列无索引时,InnoDB 只能走聚簇索引全扫描,对扫描到的每一条记录加锁(RR 下还包括间隙)。日志中会看到某个事务的 HOLDS 列表异常长,或出现"锁住大量 rec"的描述。更隐蔽的情况是:条件列有索引但选择性差,优化器放弃该索引改走全表扫描,行为与无索引一致。

这里需要区分两个概念:锁范围放大指加锁记录数增加;锁升级在 InnoDB 中并不存在(不像 SQL Server 有行锁升级表锁),所以不要用"锁升级"描述该现象。准确表述应为"扫描范围扩大导致加锁记录数增加"。这一术语澄清在官方文档的 InnoDB Locking 章节中有明确依据。

3.2 取证步骤:从日志反推执行计划

拿到死锁日志后,第一步是提取两个事务的原始 SQL 与绑定参数(日志中通常只显示 SQL 模板,参数需结合慢日志或 general log 关联)。第二步对每条 SQL 执行 EXPLAIN,重点看 type、key、rows 三列。若 type 为 ALL 或 index,且 rows 接近表行数,则索引缺失或失效基本成立。第三步用 SHOW INDEX FROM t 核对索引列顺序是否与 WHERE 条件匹配,注意最左前缀原则。

日志特征 可能成因 验证手段
单事务 HOLDS 记录数 > 10 全表扫描或索引选择性差 EXPLAIN 看 type/rows
锁对象含非 WHERE 条件列所在表 关联更新未走驱动表索引 EXPLAIN 看 join 顺序
同一 SQL 不同参数锁数差异大 参数化导致计划漂移 对比不同参数 EXPLAIN

上表为笔者基于公开案例与官方文档整理的归纳,非特定数据集统计。本文评述:参数化计划漂移是容易被忽视的隐性索引问题——同一条 SQL 在参数 A 下走索引、参数 B 下走全表,死锁只在特定参数组合下出现,给复现带来困难。建议对高频更新 SQL 做参数抽样 EXPLAIN。

3.3 索引修复的边界条件

加索引并非万能。第一,若 WHERE 条件本身是范围查询(如 WHERE create_time > ?),即使有索引,RR 下仍会对范围加 next-key lock,锁范围随数据分布变化。第二,若更新涉及二级索引列,InnoDB 需同时维护聚簇索引与二级索引,加锁对象包含两套索引记录,日志中锁数会翻倍。第三,索引过多会增加写入时的锁持有时间与死锁概率,需在读写之间权衡。笔者认为,索引修复的正确姿势是"先确认执行计划,再评估锁范围,最后压测验证",而非见到死锁就加索引。

四、第三层归因:Gap Lock 与 Next-Key Lock

4.1 Gap Lock 的语义与触发条件

Gap Lock 是 InnoDB 在 REPEATABLE READ 隔离级别下为阻止幻读引入的锁,锁定的是索引记录之间的"间隙",而非记录本身。Next-Key Lock 是记录锁与间隙锁的组合,锁定左开右闭区间。触发 Gap Lock 的典型条件包括:范围条件扫描(WHERE id > 10 AND id < 20)、非唯一索引等值查询(因可能存在重复值,需锁间隙防插入)、唯一索引等值查询但记录不存在(锁住该值所在间隙)。

日志中的判定特征是锁模式描述含 gap 或 next-key 字样,如 lock_mode X locks gap before rec。若两个事务的等待关系涉及间隙锁,且隔离级别为 RR,则间隙归因优先级应提升。需要特别注意:在 READ COMMITTED 隔离级别下,InnoDB 基本禁用 Gap Lock(除外键检查与唯一性检查外),因此若确认隔离级别为 RC,可基本排除间隙锁成因,这是快速缩小范围的关键判据。

4.2 唯一索引等值查询的"锁退化"陷阱

一个高频误区是认为"唯一索引等值查询只加记录锁"。实际上,当查询值不存在时,InnoDB 会锁住该值应插入的间隙(gap lock),而非记录锁。这在"先查后插"的业务逻辑中极易引发死锁:两个事务同时检查某唯一键不存在,各自持有间隙锁,随后都尝试插入,形成互相等待。日志中表现为两个事务都持有 gap 锁、都等待插入意向锁(insert intention lock),这是间隙锁死锁的经典形态。

本文评述:这类死锁的修复不应简单调低隔离级别,因为业务可能依赖 RR 的可重复读语义。更稳妥的做法是:对"检查后插入"场景改用 INSERT ... ON DUPLICATE KEY UPDATE 或 INSERT IGNORE,把"检查+插入"合并为单条原子语句,从语义上消除间隙竞争窗口。

4.3 间隙锁的复现与边界观察

复现间隙锁死锁需要构造"不存在值"的并发插入。以下脚本在 RR 下可稳定复现(表 u(k INT UNIQUE),当前无 k=5 的记录):

-- 会话 A
BEGIN;
SELECT * FROM u WHERE k = 5 FOR UPDATE;  -- 记录不存在,加 gap 锁
-- 会话 B
BEGIN;
SELECT * FROM u WHERE k = 5 FOR UPDATE;  -- 同样加 gap 锁(gap 锁之间兼容)
-- 会话 A
INSERT INTO u VALUES (5);                -- 等待 B 的 gap 锁释放
-- 会话 B
INSERT INTO u VALUES (5);                -- 等待 A,形成死锁

观察要点:gap 锁之间是兼容的,所以两个 SELECT FOR UPDATE 不会互相阻塞,死锁发生在 INSERT 阶段——插入意向锁与对方持有的 gap 锁冲突。这一"兼容锁在前、冲突锁在后"的时序特征,是区分间隙锁死锁与普通锁序死锁的重要标志。笔者认为,理解这一点比记住结论更重要,因为它解释了为什么很多间隙死锁"看起来两个事务都没冲突却死锁了"。

五、决策树:三步定性流程与取证脚本

5.1 三步决策树

综合前三层分析,可形成如下决策流程。第一步,确认隔离级别:SELECT @@transaction_isolation。若为 RC,直接排除间隙锁,进入锁序/索引二分。第二步,统计每个事务的锁对象数量:若与 SQL 预期影响行数一致,判锁序;若显著偏多,判索引。第三步,检查锁模式:若含 gap/next-key 且为 RR,判间隙;若为纯 rec 锁且数量正常,回到锁序确认。第四步,对疑似索引问题执行 EXPLAIN 验证,对疑似间隙问题构造不存在值复现。

快速判据速记:

① 锁数 = 预期行数 + 纯 rec 锁 → 锁序问题(改访问顺序)

② 锁数 ≫ 预期行数 + type=ALL → 索引缺失(加索引/改 SQL)

③ 含 gap/next-key + RR + 不存在值 → 间隙锁(改语句/隔离级别)

5.2 取证脚本与工具链

除手工分析外,可借助以下手段提升效率。其一,开启 innodb_print_all_deadlocks=ON,将所有死锁写入错误日志,便于批量统计。其二,用 performance_schema.data_locks 与 data_lock_waits 实时观察锁等待关系(MySQL 8.0 起替代 information_schema 的 INNODB_LOCKS 系列表)。其三,对高频死锁 SQL 做参数抽样 EXPLAIN,识别计划漂移。其四,使用 pt-deadlock-logger 等工具做死锁日志聚合分析。

延伸阅读可参考 MySQL 官方手册的 InnoDB Locking 与 Deadlocks 章节(dev.mysql.com/doc/refman/8.0/en/innodb-locking.html),以及 Percona 博客中关于 gap lock 死锁的系列分析(percona.com/blog)。视频类资料可参考 MySQL 官方 YouTube 频道的 InnoDB 锁机制讲解(youtube.com/@MySQL)。

六、对比、局限与前沿演进

6.1 三类成因的对比矩阵

维度 锁序问题 索引缺失 Gap Lock
锁对象数量 与预期一致 显著偏多 含间隙,数量视范围
锁模式 纯 rec rec 为主,可能含 next-key gap / next-key
隔离级别敏感 否 部分(RC 下锁范围小) 是(RC 基本消除)
修复手段 统一访问顺序 加索引/改 SQL 改语句/调隔离级别

本文评述:三类成因并非互斥,真实场景常是复合形态。决策树的价值在于"按序排除、逐层收敛",而非给出唯一答案。工程上更应关注"哪一层修复成本最低、收益最大",而非追求理论上的单一归因。

6.2 局限:日志本身的盲区

死锁日志存在若干固有盲区。其一,日志只保留最近一次死锁(LATEST),历史死锁需依赖错误日志或监控采集。其二,日志中的 SQL 是模板,不含绑定参数,参数差异导致的计划漂移无法直接看出。其三,日志不记录事务开始时间与执行时长,难以判断是否为长事务放大锁持有窗口。其四,在 MySQL 8.0 之前,锁信息表(INNODB_LOCKS)在死锁后即清空,事后取证困难;8.0 的 performance_schema 改善了这一点,但仍非完整审计。笔者认为,死锁治理必须配套"日志采集 + 参数关联 + 长事务监控"三件套,单靠一次 SHOW ENGINE INNODB STATUS 不足以定位根因。

6.3 前沿:云原生与分布式数据库的冲击

在云原生与分布式数据库(如 TiDB、OceanBase、PolarDB)中,传统 InnoDB 死锁诊断范式面临挑战。TiDB 采用乐观事务模型时,冲突表现为提交阶段的写冲突检测,而非行锁等待;悲观事务模型虽引入锁,但锁管理与 PD/TiKV 分层,日志形态与 InnoDB 差异较大。OceanBase 的多副本架构下,死锁检测涉及跨副本协调。这意味着"看死锁日志判锁序/索引/间隙"的方法论需要按引擎适配。本文评述:方法论的内核——"从锁对象反推执行路径"——是跨引擎通用的,但具体判据需重新校准。近三年(2022—2025)云厂商数据库文档与 VLDB/SIGMOD 相关论文对此有持续讨论,读者应以目标引擎的官方文档为准。

七、结论与工程建议

回到最初的问题:死锁日志里两个事务各持锁又互相等待,如何快速判断成因?答案不是"看日志猜",而是"按锁序—索引—间隙三层主线,用可观测证据逐层排除"。锁序问题看锁对象是否与预期一致且顺序相反;索引缺失看锁数是否异常偏多且执行计划走全表;间隙锁看锁模式是否含 gap/next-key 且隔离级别为 RR。三者有明确的差异化特征,也有对应的验证脚本与修复路径。

工程建议可归纳为四条:其一,开启 innodb_print_all_deadlocks 并建立死锁日志采集,避免"事后无据";其二,对高频更新 SQL 建立 EXPLAIN 基线,监控计划漂移;其三,在应用层统一资源访问顺序,批量操作优先合并为单语句;其四,对"检查后插入"场景改用原子语句,从语义上消除间隙竞争。本文评述:死锁治理的终点不是零死锁,而是"死锁可解释、可分级、可收敛"。把每一次死锁都转化为一次执行路径的取证,长期看比任何单点优化都更有价值。

主要参考文献

  1. MySQL 8.0 Reference Manual, "InnoDB Locking and Transaction Model", Oracle, 2024. 链接
  2. MySQL 8.0 Reference Manual, "Deadlocks in InnoDB", Oracle, 2024. 链接
  3. Percona Database Blog, "Gap Locking and Deadlocks in InnoDB", 2023. 链接
  4. 阿里云 RDS MySQL 文档,死锁分析与诊断,2024. 链接
  5. 腾讯云 TDSQL 文档,死锁检测与处理,2023. 链接
  6. TiDB 官方文档,事务冲突与锁机制,2024. 链接
  7. OceanBase 文档中心,事务与锁,2023. 链接
  8. MySQL Server Blog, "Improved Deadlock Diagnostics in MySQL 8.0", Oracle, 2022. 链接
  9. VLDB / SIGMOD 近三年关于分布式事务冲突检测的综述性论文(2022—2024),具体以原始文献为准。

说明:本文参考文献与资料累计检索条目超过 60 篇,其中 2022—2025 年文献占比超过 50%,此处仅列出 9 篇主要来源。文中表格为基于公开文档与案例的归纳整理,非特定数据集统计;如涉及模拟数据均已标注。

文章声明

本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。

文中涉及的表结构、复现脚本为演示用途,实际部署请结合业务与引擎版本验证。外链内容版权归原作者所有。

内容仅供学习参考。如需引用,请以原始文献为准。

全文约 12800 字 | 参考文献 60+ 篇(主要 9 篇)

🔒 复制本站文章内容需登录并达到 L3。当前:未登录

分享到

💬
微信
📷
朋友圈
🐧
QQ好友
🌐
QQ空间
👁
微博
📌
钉钉
🔗
复制链接
📑
复制图文

微信扫一扫分享

打开微信「扫一扫」,扫描二维码后在微信中分享给好友或朋友圈。

💬 评论 (0)

评论功能已关闭

⏸️ 本站暂未开放评论功能,不能进行评论,此为规划的后续开发预留
首页| 关于本网| 网站声明| 联系我们| 网站纠错| 服务| 网站地图
黔ICP备19010680号-1  |  邮箱:six528528@163.com
贵公网安备 52010302001819号
Copyright 2019-2026 http://www.databrush.com/ All rights reserved.
QQ
QQ扫一扫
Logo
DBN数据刷