MySQL 间隙锁死锁问题的原理、场景与完整解决方案间隙锁死锁是 InnoDB 引擎在可重复读RR隔离级别下的高频死锁场景它的成因和普通行锁死锁不同核心根源是「间隙锁之间兼容但间隙锁与插入意向锁互斥」的特性极易在「先查后插」的并发场景下触发循环等待。一、核心原理铺垫1. 什么是间隙锁间隙锁Gap Lock是 InnoDB 在 RR 隔离级别下为了解决幻读问题引入的锁机制它锁的不是具体的数据行而是索引之间的空闲间隙防止其他事务往间隙中插入新数据。触发条件使用当前读SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE、DELETE时如果查询条件未命中具体行或者是范围查询就会对命中的间隙加锁。关键特性间隙锁与间隙锁之间是兼容的——多个事务可以同时持有同一个间隙的间隙锁不会互相阻塞。2. 为什么间隙锁会导致死锁和间隙锁配对的还有「插入意向锁Insert Intention Lock」事务执行INSERT时会先向目标间隙申请插入意向锁。核心互斥规则间隙锁 ↔ 插入意向锁 互斥。由此就形成了经典的死锁闭环事务 A 对间隙 G 加间隙锁成功无阻塞事务 B 对间隙 G 加间隙锁成功间隙锁之间兼容事务 A 往间隙 G 插入数据申请插入意向锁 → 被事务 B 的间隙锁阻塞事务 B 往间隙 G 插入数据申请插入意向锁 → 被事务 A 的间隙锁阻塞→ 循环等待死锁触发。二、最常见的 3 种间隙锁死锁场景场景1「不存在则插入」的去重逻辑最高频这是业务开发中最容易踩的坑典型代码逻辑先通过SELECT ... FOR UPDATE判断记录是否存在不存在则执行INSERT。示例表user主键为id已有数据 id1、id5间隙为 (1,5)时间线事务A事务BT1SELECT * FROM user WHERE id3 FOR UPDATE;id3不存在对间隙(1,5)加X型间隙锁T2SELECT * FROM user WHERE id3 FOR UPDATE;同样对间隙(1,5)加间隙锁成功不阻塞T3INSERT INTO user(id,name) VALUES(3,a);申请插入意向锁被事务B的间隙锁阻塞等待T4INSERT INTO user(id,name) VALUES(3,b);申请插入意向锁被事务A的间隙锁阻塞等待T5死锁触发InnoDB自动回滚其中一个事务场景2范围更新 反向插入两个事务对相反方向的范围加锁随后互相向对方的锁范围内插入数据形成交叉间隙锁等待。例如事务AUPDATE goods SET stockstock-1 WHERE id 10对 (10, ∞) 加间隙锁事务BUPDATE goods SET stockstock-1 WHERE id 20对 (-∞, 20) 加间隙锁随后事务A插入id15事务B插入id12双方都需要对方间隙内的插入意向锁形成死锁。场景3无索引/低区分度索引导致全表间隙锁如果更新/查询条件不走索引InnoDB 会进行全表扫描对整张表的所有间隙都加上间隙锁相当于锁了全表的插入能力任何并发插入都极易触发死锁。三、死锁排查方法1. 查看实时死锁日志执行以下命令查看最近一次死锁的详细信息这是定位根因的核心手段SHOWENGINEINNODBSTATUS;重点关注LATEST DETECTED DEADLOCK段落查看两个事务分别持有的锁类型gap lock代表间隙锁insert intention代表插入意向锁查看锁的范围索引区间确认是否为同一个间隙查看触发死锁的具体 SQL对应到业务代码2. 持久化死锁日志开启全局配置让所有死锁都记录到 MySQL 错误日志中方便回溯排查SETGLOBALinnodb_print_all_deadlocksON;该配置线上可动态开启无需重启建议生产环境默认开启。3. 辅助定位手段结合业务日志确认死锁发生时的并发接口、请求参数还原并发执行路径。检查对应 SQL 的执行计划确认是否走索引、索引区分度如何。四、分级解决方案按推荐优先级排序方案1代码层优化从根源规避「先查后插」逻辑最有效针对最高频的「不存在则插入」场景用原子 UPSERT 语法替代「先查后插」彻底打破「先加间隙锁、再申请插入意向锁」的死锁路径。推荐写法INSERT … ON DUPLICATE KEY UPDATE-- 原逻辑先SELECT FOR UPDATE判断再INSERT-- 优化后原子插入冲突则更新INSERTINTOuser(id,name,update_time)VALUES(3,a,NOW())ONDUPLICATEKEYUPDATEnameVALUES(name),update_timeNOW();为什么能避免死锁该语句是原子操作直接尝试插入数据若唯一键冲突则对已存在的行加记录锁并执行更新。不会出现「两个事务同时持有间隙锁」的阶段第二个事务只会等待第一个事务的行锁不会形成循环等待。注意事项必须依赖唯一索引/主键才能触发冲突更新逻辑。不推荐使用REPLACE INTO它本质是「删除旧行插入新行」会导致主键自增跳跃、触发器异常执行等副作用。方案2统一加锁顺序缩小锁范围如果业务必须保留「先查后插」逻辑按以下规则优化所有事务按相同顺序加锁比如统一按 ID 从小到大的顺序执行更新/查询避免反向范围加锁导致的交叉间隙重叠。用精准等值查询替代范围查询范围越大间隙锁覆盖范围越广冲突概率越高。能精准命中行的就不要用范围条件。缩小事务粒度事务内只保留核心的数据库操作减少锁的持有时间降低冲突概率。方案3索引优化避免全表间隙锁确保所有当前读语句都走索引给WHERE条件的字段建立合适的索引无索引会导致全表扫描全表间隙加锁死锁概率指数级上升。优先使用唯一索引唯一索引的等值查询命中行时只会加记录锁不会产生间隙锁普通索引的间隙锁范围更大冲突概率更高。避免低区分度索引区分度差的索引会导致单次查询锁定大量间隙极易和其他事务重叠。方案4调整隔离级别为 RC从根源消除间隙锁如果业务可以接受「不可重复读」将隔离级别从 RR 下调为读已提交RC是性价比极高的方案RC 级别下InnoDB 会关闭间隙锁仅外键和唯一键校验保留间隙锁从根源上消除间隙锁死锁。RC 级别锁粒度更小并发性能更高是互联网企业的主流选型。# my.cnf 配置 transaction_isolation READ-COMMITTED适用前提业务不需要严格的可重复读绝大多数业务系统电商、办公、后台系统都满足。方案5应用层兜底死锁自动重试死锁无法 100% 杜绝必须做兜底处理在业务代码中捕获 MySQL 死锁异常错误码1213实现指数退避的自动重试机制重试2~3次即可。因为 InnoDB 检测到死锁后会自动回滚代价更小的事务重试后通常可以正常执行。五、常见误区澄清误区唯一索引不会产生间隙锁错误。唯一索引的等值查询若未命中任何记录依然会对命中的间隙加间隙锁只有命中了具体的行才只会加记录锁。误区间隙锁之间是互斥的错误。间隙锁之间互相兼容多个事务可以同时持有同一个间隙的锁真正互斥的是间隙锁和插入意向锁。误区关闭间隙锁会导致严重幻读RC 级别下通过 MVCC 快照读绝大多数业务场景感知不到幻读只有当前读场景下可能出现幻读业务影响极小远低于频繁死锁带来的故障影响。六、企业级最佳实践总结优先选型业务无强一致要求时默认使用 RC 隔离级别从根源减少间隙锁问题。代码规范高并发去重插入场景强制使用INSERT ... ON DUPLICATE KEY UPDATE禁止「SELECT FOR UPDATE INSERT」的写法。索引底线所有 UPDATE、DELETE、FOR UPDATE 查询必须走索引定期巡检慢SQL与全表扫描。兜底保障应用层统一封装死锁重试逻辑同时开启死锁日志持久化便于问题回溯。
MySQL 间隙锁死锁问题处理
MySQL 间隙锁死锁问题的原理、场景与完整解决方案间隙锁死锁是 InnoDB 引擎在可重复读RR隔离级别下的高频死锁场景它的成因和普通行锁死锁不同核心根源是「间隙锁之间兼容但间隙锁与插入意向锁互斥」的特性极易在「先查后插」的并发场景下触发循环等待。一、核心原理铺垫1. 什么是间隙锁间隙锁Gap Lock是 InnoDB 在 RR 隔离级别下为了解决幻读问题引入的锁机制它锁的不是具体的数据行而是索引之间的空闲间隙防止其他事务往间隙中插入新数据。触发条件使用当前读SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE、DELETE时如果查询条件未命中具体行或者是范围查询就会对命中的间隙加锁。关键特性间隙锁与间隙锁之间是兼容的——多个事务可以同时持有同一个间隙的间隙锁不会互相阻塞。2. 为什么间隙锁会导致死锁和间隙锁配对的还有「插入意向锁Insert Intention Lock」事务执行INSERT时会先向目标间隙申请插入意向锁。核心互斥规则间隙锁 ↔ 插入意向锁 互斥。由此就形成了经典的死锁闭环事务 A 对间隙 G 加间隙锁成功无阻塞事务 B 对间隙 G 加间隙锁成功间隙锁之间兼容事务 A 往间隙 G 插入数据申请插入意向锁 → 被事务 B 的间隙锁阻塞事务 B 往间隙 G 插入数据申请插入意向锁 → 被事务 A 的间隙锁阻塞→ 循环等待死锁触发。二、最常见的 3 种间隙锁死锁场景场景1「不存在则插入」的去重逻辑最高频这是业务开发中最容易踩的坑典型代码逻辑先通过SELECT ... FOR UPDATE判断记录是否存在不存在则执行INSERT。示例表user主键为id已有数据 id1、id5间隙为 (1,5)时间线事务A事务BT1SELECT * FROM user WHERE id3 FOR UPDATE;id3不存在对间隙(1,5)加X型间隙锁T2SELECT * FROM user WHERE id3 FOR UPDATE;同样对间隙(1,5)加间隙锁成功不阻塞T3INSERT INTO user(id,name) VALUES(3,a);申请插入意向锁被事务B的间隙锁阻塞等待T4INSERT INTO user(id,name) VALUES(3,b);申请插入意向锁被事务A的间隙锁阻塞等待T5死锁触发InnoDB自动回滚其中一个事务场景2范围更新 反向插入两个事务对相反方向的范围加锁随后互相向对方的锁范围内插入数据形成交叉间隙锁等待。例如事务AUPDATE goods SET stockstock-1 WHERE id 10对 (10, ∞) 加间隙锁事务BUPDATE goods SET stockstock-1 WHERE id 20对 (-∞, 20) 加间隙锁随后事务A插入id15事务B插入id12双方都需要对方间隙内的插入意向锁形成死锁。场景3无索引/低区分度索引导致全表间隙锁如果更新/查询条件不走索引InnoDB 会进行全表扫描对整张表的所有间隙都加上间隙锁相当于锁了全表的插入能力任何并发插入都极易触发死锁。三、死锁排查方法1. 查看实时死锁日志执行以下命令查看最近一次死锁的详细信息这是定位根因的核心手段SHOWENGINEINNODBSTATUS;重点关注LATEST DETECTED DEADLOCK段落查看两个事务分别持有的锁类型gap lock代表间隙锁insert intention代表插入意向锁查看锁的范围索引区间确认是否为同一个间隙查看触发死锁的具体 SQL对应到业务代码2. 持久化死锁日志开启全局配置让所有死锁都记录到 MySQL 错误日志中方便回溯排查SETGLOBALinnodb_print_all_deadlocksON;该配置线上可动态开启无需重启建议生产环境默认开启。3. 辅助定位手段结合业务日志确认死锁发生时的并发接口、请求参数还原并发执行路径。检查对应 SQL 的执行计划确认是否走索引、索引区分度如何。四、分级解决方案按推荐优先级排序方案1代码层优化从根源规避「先查后插」逻辑最有效针对最高频的「不存在则插入」场景用原子 UPSERT 语法替代「先查后插」彻底打破「先加间隙锁、再申请插入意向锁」的死锁路径。推荐写法INSERT … ON DUPLICATE KEY UPDATE-- 原逻辑先SELECT FOR UPDATE判断再INSERT-- 优化后原子插入冲突则更新INSERTINTOuser(id,name,update_time)VALUES(3,a,NOW())ONDUPLICATEKEYUPDATEnameVALUES(name),update_timeNOW();为什么能避免死锁该语句是原子操作直接尝试插入数据若唯一键冲突则对已存在的行加记录锁并执行更新。不会出现「两个事务同时持有间隙锁」的阶段第二个事务只会等待第一个事务的行锁不会形成循环等待。注意事项必须依赖唯一索引/主键才能触发冲突更新逻辑。不推荐使用REPLACE INTO它本质是「删除旧行插入新行」会导致主键自增跳跃、触发器异常执行等副作用。方案2统一加锁顺序缩小锁范围如果业务必须保留「先查后插」逻辑按以下规则优化所有事务按相同顺序加锁比如统一按 ID 从小到大的顺序执行更新/查询避免反向范围加锁导致的交叉间隙重叠。用精准等值查询替代范围查询范围越大间隙锁覆盖范围越广冲突概率越高。能精准命中行的就不要用范围条件。缩小事务粒度事务内只保留核心的数据库操作减少锁的持有时间降低冲突概率。方案3索引优化避免全表间隙锁确保所有当前读语句都走索引给WHERE条件的字段建立合适的索引无索引会导致全表扫描全表间隙加锁死锁概率指数级上升。优先使用唯一索引唯一索引的等值查询命中行时只会加记录锁不会产生间隙锁普通索引的间隙锁范围更大冲突概率更高。避免低区分度索引区分度差的索引会导致单次查询锁定大量间隙极易和其他事务重叠。方案4调整隔离级别为 RC从根源消除间隙锁如果业务可以接受「不可重复读」将隔离级别从 RR 下调为读已提交RC是性价比极高的方案RC 级别下InnoDB 会关闭间隙锁仅外键和唯一键校验保留间隙锁从根源上消除间隙锁死锁。RC 级别锁粒度更小并发性能更高是互联网企业的主流选型。# my.cnf 配置 transaction_isolation READ-COMMITTED适用前提业务不需要严格的可重复读绝大多数业务系统电商、办公、后台系统都满足。方案5应用层兜底死锁自动重试死锁无法 100% 杜绝必须做兜底处理在业务代码中捕获 MySQL 死锁异常错误码1213实现指数退避的自动重试机制重试2~3次即可。因为 InnoDB 检测到死锁后会自动回滚代价更小的事务重试后通常可以正常执行。五、常见误区澄清误区唯一索引不会产生间隙锁错误。唯一索引的等值查询若未命中任何记录依然会对命中的间隙加间隙锁只有命中了具体的行才只会加记录锁。误区间隙锁之间是互斥的错误。间隙锁之间互相兼容多个事务可以同时持有同一个间隙的锁真正互斥的是间隙锁和插入意向锁。误区关闭间隙锁会导致严重幻读RC 级别下通过 MVCC 快照读绝大多数业务场景感知不到幻读只有当前读场景下可能出现幻读业务影响极小远低于频繁死锁带来的故障影响。六、企业级最佳实践总结优先选型业务无强一致要求时默认使用 RC 隔离级别从根源减少间隙锁问题。代码规范高并发去重插入场景强制使用INSERT ... ON DUPLICATE KEY UPDATE禁止「SELECT FOR UPDATE INSERT」的写法。索引底线所有 UPDATE、DELETE、FOR UPDATE 查询必须走索引定期巡检慢SQL与全表扫描。兜底保障应用层统一封装死锁重试逻辑同时开启死锁日志持久化便于问题回溯。