MySQL RC 隔离级别下唯一索引与间隙锁引发的死锁分析
在 MySQL REPEATABLE READ (RR) 以外的读已提交 (RC) 隔离级别中,通常认为不会使用间隙锁,但在涉及唯一联合索引的"先删后增"操作中,仍可能因唯一键冲突检查机制产生间隙锁,进而导致事务间互相等待形成死锁。本文将深入分析这一现象的原理并提供代码层面的优化方案。
问题背景
某业务场景中,MQ 接收商品主数据时需同步更新附属信息表(一对多关系)。原实现逻辑为:先删除旧数据,再批量插入新数据。在线上运行过程中,监控平台频繁抛出死锁警告。
死锁深度分析
第 1 步:解读死锁日志
根据线上捕获的死锁日志,两个事务均执行了相同的 INSERT ... ON DUPLICATE KEY UPDATE 语句:
INSERT INTO MD_CMMDTY_OTHER19(
cmmdty_code, business_field, business_field_desc, keyword_code,
lastmodifier, lastmodified
) VALUES ('12256633711', 'TAX_CODE', ...)
ON DUPLICATE KEY UPDATE ...
日志显示:
- 事务 1:正在请求索引
idx_cmmdty_code_business_field上的 X 锁(排他锁),状态为waiting。 - 事务 2:持有相关记录锁,但同样在请求同一索引的 X 锁,状态也为
waiting。
由于两个事务都在等待对方释放资源,且都持有部分资源,构成了典型的死锁条件,InnoDB 最终回滚了事务 2。
第 2 步:排查隔离级别与加锁机制
很多人认为 RC 隔离级别下间隙锁(Gap Lock)会自动失效,因此不会发生此类死锁。然而 MySQL 官方文档指出,虽然搜索和扫描时不启用间隙锁,但在外键约束检查和重复键(Duplicate-key)检查时,仍会使用间隙锁。
在本案例中,引起死锁的关键在于:先执行的 Delete 操作 + 唯一联合索引。
通过分析源码逻辑发现:
- 当事务在执行
DELETE时,如果存在唯一键冲突,InnoDB 需要对被标记为删除的记录及其前后的间隙添加间隙锁,以防止其他事务插入重复键。 - 在并发场景下,如果一个事务刚刚删除了某条记录,另一个事务紧接着尝试插入相同或相邻的唯一键值,就会触发了基于间隙锁的互斥检查。
第 3 步:锁定冲突的具体过程
结合本地复现和源码追踪,死锁产生的具体步骤如下:
- 获取间隙锁:事务 A 在删除记录后,为了维持唯一性,获取了特定区间(如 Next-Key)的间隙锁。
- 冲突竞争:此时,事务 B 尝试插入相同键值的记录。由于事务 A 持有的锁模式与事务 B 所需的锁模式不一致,事务 B 进入等待队列。
- 形成闭环:反之亦然。当两个相邻的唯一键值在同一时间窗内被不同事务处理时,它们可能在同一组索引页面上争夺
X waiting锁,从而触发 InnoDB 的死锁检测并回滚其中一个事务。
为什么一定要在 Delete-marked 之后加锁?
关于源码为何在标记为删除的记录后强制加入间隙锁,主要目的是为了防止脏读或在回滚时的数据一致性问题。即使是在 RC 级别下,为了保证唯一索引的一致性校验不受并发插入干扰,必须锁定该间隙。
解决方案
既然死锁根源在于“先删后增”导致的间隙锁竞争,优化的核心思路就是改变数据更新的逻辑,避免在高频并发下的直接物理删除。
- 合并变更操作:MQ 下发的通常为全量或部分全量数据。建议将数据处理逻辑改为:
- 对于 MQ 下发的数据,若数据库中存在则执行UPDATE;
- 若不存在则执行INSERT;
- 对于数据库中已存在但 MQ 未下发的冗余数据,可延迟清理或直接覆盖整个集合(视业务对时效性的要求而定)。 - 减少锁竞争:通过这种“判断是否存在”的方式,避开了显式
DELETE带来的唯一键间隙锁开销,大大降低了死锁概率。
以上就是 MySQL RC 级别下因唯一索引和间隙锁引发死锁的详细分析与解决资料,更多关于数据库性能优化的内容请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













