MySQL Insert on Duplicate Key Update 死锁定位与解决方案

拾光
拾光
发布时间:2026-10-16 | 6人浏览过
原创

在 MySQL 高并发写入场景中,INSERT ... ON DUPLICATE KEY UPDATE 语句因混合使用间隙锁和插入意向锁,极易引发死锁。本文详细剖析了其底层锁机制冲突原理,并通过锁兼容性表格与事务还原示例解释死锁形成过程,最终给出切换为普通 Insert+异常重试或业务限流的实用解决方案。

问题背景与定位

在线上监测中发现,某 MySQL 业务数据库频繁出现间歇性死锁日志。经分析,死锁根源在于并发执行 INSERT ... ON DUPLICATE KEY UPDATE 语句时触发的锁冲突。生产环境使用 MySQL 5.7,事务隔离级别为默认的行重复度(RR, Repeatable Read)。

第1步:确认表结构与触发 SQL

首先查看导致问题的表结构及对应的 SQL 语句。表中存在一个基于手机号 phone 的唯一索引,这是冲突的关键点。

CREATE TABLE IF NOT EXISTS `user_info` (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    name VARCHAR(20) NOT NULL,
    phone BIGINT(20) UNSIGNED NOT NULL,
    update_time timestamp NOT NULL,
    UNIQUE KEY phone (phone)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

引发死锁的具体 SQL 如下:

insert into user_info (name, phone, update_time) values (X,Y,Z) 
on duplicate key update update_time=Z;

第2步:查看死锁诊断信息

当发生死锁后,执行 show engine innodb status 查看日志,发现关键报错信息:lock_mode X locks gap before rec insert intention waiting。这表明事务正在等待获取“间隙锁”以申请“插入意向锁”,暗示了 Gap Lock 导致的阻塞。

原理分析:Lock 机制

INSERT ... ON DUPLICATE KEY UPDATE 的语义是“存在则更新,不存在则插入”。但在 RR 隔离级别下,其加锁逻辑与普通 INSERT 不同:

  • 若记录存在:获取 Next-Key Lock(记录锁 + 间隙锁),锁定该索引记录及其前方间隙。
  • 若记录不存在:在唯一键上获取 Gap Lock(间隙锁)以及 Insert Intention Lock(插入意向锁)。

其中,Gap Lock 用于防止其他事务插入到该间隙中。例如,若现有唯一键值为 1、5、10: - 插入 4 时,Gap Lock 范围是 (1, 5)。 - 插入 15 时,Gap Lock 范围是 (10, +∞)。

锁兼容性矩阵

理解以下锁兼容性矩阵是理解死锁的核心:

当前持有 \ 后续申请 插入意向锁 行锁 (Record Lock) Gap Lock
插入意向锁 兼容 互斥 互斥
行锁 兼容 互斥 兼容
Gap Lock 兼容 兼容 兼容

关键点在于:持有 Gap Lock 的事务会拒绝其他事务获取同一范围的插入意向锁。而在 ON DUPLICATE KEY 场景中,多个并发事务往往都试图向同一巨大间隙(如末尾)插入新记录,从而产生锁等待循环。

死锁场景还原

假设数据库中已有手机号 157xxxxxx00、157xxxxxx1、157xxxxxx2。我们模拟三个事务 tx1, tx2, tx3 并发尝试插入新的递增手机号:

阶段 tx1 动作 tx2 动作 tx3 动作
1 插入 157xxxxxx1 (Gap: x00~x01) - -
1a 持有 Gap Lock (157xxxxxx1, +∞) - -
2 - 插入 157xxxxxx2 -
2a - 申请插入意向锁失败(被tx1 Gap Lock排斥),成功获取 Gap Lock,进入等待 -
3 - - 插入 157xxxxxx3
3a - - 申请插入意向锁失败(被tx1或tx2排斥),成功获取 Gap Lock,进入等待
4 Commit,释放所有锁 - -
5 - 获得插入意向锁,准备提交 获得插入意向锁,准备提交
6 - tx2 与 tx3 均处于最后一步,相互检测对方持有的资源无法获取,触发死锁检测,其中一个回滚。

这种因有序插入产生的“排队效应”,使得多个事务几乎同时在同一个大间隙尾部争抢锁资源,从而形成死锁。

问题解决与优化方案

由于 INSERT ... ON DUPLICATE KEY UPDATE 本身原子性强且语法简洁,但在高并发无序或近有序插入下稳定性较差。建议根据业务场景选择以下两种解决方案之一:

方案一:应用层改造(推荐)

抛弃 ON DUPLICATE KEY,改用普通 INSERT 语句:

  1. 执行标准 INSERT INTO user_info (...) VALUES (...)。
  2. 捕获 SQL 异常(如 Duplicate entry for key 'phone')。
  3. 若捕获异常,则转为执行 UPDATE user_info SET ... WHERE phone = ? 或在应用层进行重试。

虽然代码量增加,但避免了数据库层面的复杂锁竞争,将逻辑控制在应用层更可控。

方案二:业务层限流与分片

如果必须使用原 SQL,需从业务入口减少并发密度:

  • 接口限流:限制特定时间窗口内的最大请求数。
  • 批量合并:在事务内对即将插入的数据列表进行排序去重,或将大批量数据拆分为小批次分批执行,避免海量并发请求同时到达数据库同一索引区域。

总结

INSERT ... ON DUPLICATE KEY UPDATE 虽好,但在高并发写入大量新增数据的场景下并非最佳选择。其背后隐藏的间隙锁与插入意向锁的交互机制容易在相同 Range 内引发死锁。开发者应意识到性能权衡,根据实际并发情况选择合适的替代方案。以上就是关于 MySQL INSERT ... ON DUPLICATE KEY UPDATE 死锁问题定位与解决的详细内容,更多关于 MySQL 高并发处理技巧的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表