间隙锁何时出现:用范围更新解释幻读保护边界
来源:17golang原创
时间:2026-10-07 15:15:54 313浏览 收藏
间隙锁最典型地出现在 InnoDB 的锁定读、UPDATE 或 DELETE 使用范围条件扫描索引时。在默认的 REPEATABLE READ 隔离级别下,InnoDB 不只锁住已经存在的索引记录,还可能保护记录之间以及范围边缘的间隙,避免别的事务插入满足同一范围的新记录。精确命中唯一索引的一条记录时,通常只需要记录锁;普通 SELECT 则依靠一致性读快照,而不是靠间隙锁解决幻读。
MySQL 8.4 官方文档:https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html
判断间隙锁不能只看 SQL 里有没有 BETWEEN。真正决定锁边界的是事务隔离级别、优化器采用的索引,以及该索引实际扫描了哪些记录和间隙。
一次看似普通的范围更新为何影响新增
下面用一个库存优先级调整的演练场景说明问题。业务事务需要把优先级 20 到 40 的待处理任务统一标记。如果这条更新长时间没有提交,另一个事务尝试新增优先级 30 的任务时会等待。表中原有行是否正好存在 priority = 30 并不是关键;30 落在受保护的索引间隙内,就足以让插入等待。
-- 建立演练表,并让范围条件可以沿二级索引扫描
CREATE TABLE task_queue (
id BIGINT PRIMARY KEY,
priority INT NOT NULL,
state VARCHAR(16) NOT NULL,
KEY idx_priority (priority)
) ENGINE = InnoDB;
-- 只准备范围两侧和范围内的少量记录,便于观察间隙
INSERT INTO task_queue (id, priority, state) VALUES
(1, 10, 'pending'),
(2, 20, 'pending'),
(3, 40, 'pending'),
(4, 50, 'pending');
影响面应当表述为“与 idx_priority 扫描区间重叠的写入可能等待”,而不是“整张表被锁”。同一张表上不相交的索引区间仍可能继续并发,但是否冲突要以实际执行计划和锁信息为准。
用双事务时间线还原等待现场
先在会话 A 中使用默认的 REPEATABLE READ 开启事务并执行范围更新,暂时不提交:
-- 会话 A:显式使用 RR,锁住范围更新扫描到的索引区间 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION; -- 范围 UPDATE 会对匹配记录加排他锁,并保护相关索引间隙 UPDATE task_queue SET state = 'reviewing' WHERE priority BETWEEN 20 AND 40; -- 演练期间先不要提交,留出观察并发等待的窗口
再在会话 B 中插入一个落在区间内的新优先级:
-- 会话 B:30 位于 20 与 40 之间,会与受保护间隙发生冲突 START TRANSACTION; INSERT INTO task_queue (id, priority, state) VALUES (5, 30, 'pending'); -- 等待解除后再按演练目标提交或回滚,避免遗留长事务
此时会话 B 的插入可能进入锁等待。会话 A 提交或回滚后,等待才有机会继续。这里不是已有的 priority = 30 行被锁住,而是新索引记录准备插入的位置落在范围更新所保护的间隙里。
三个条件共同决定间隙锁是否出现
第一看隔离级别。MySQL 8.4 官方手册说明,InnoDB 默认使用 REPEATABLE READ;对锁定读、UPDATE 和 DELETE,除唯一索引唯一条件外,通常会对扫描的索引范围采用间隙锁或 next-key 锁。切换到 READ COMMITTED 后,搜索和索引扫描的间隙锁通常被关闭,只在外键约束检查和重复键检查等场景保留。
第二看访问路径。条件列有合适索引时,锁边界通常围绕该索引的扫描范围;没有可用索引时,InnoDB 可能扫描并锁住更多聚簇索引记录,使并发影响远大于业务条件表面上描述的范围。因此,先看执行计划,再讨论“锁了几行”。
-- 用 EXPLAIN 确认优化器是否选择 idx_priority 以及估算扫描范围 EXPLAIN UPDATE task_queue SET state = 'reviewing' WHERE priority BETWEEN 20 AND 40;
第三看条件是否是唯一索引的精确等值。完整唯一键命中单条记录时,InnoDB 通常只锁索引记录,不需要保护前方间隙;普通索引等值、只使用联合唯一索引的一部分列,或任何范围搜索,都不能直接套用这个例外。

根因不是 BETWEEN,而是扫描到的索引区间
很多排障记录把原因简化成“BETWEEN 会产生间隙锁”,这个说法容易误导。>、、前缀范围、普通索引等值乃至缺少合适索引的条件,都可能形成范围型扫描。反过来,即使 SQL 看起来复杂,只要最终使用完整唯一索引精确定位一条记录,也可能只产生记录锁。
在 InnoDB 中,next-key 锁可以理解为“某条索引记录的记录锁 + 该记录前方的间隙锁”。它保护的是索引中的位置范围,使另一个事务不能在受保护区间内插入新的索引记录。纯间隙锁本身主要抑制插入;它不是对现有记录内容的排他修改许可,也不要把它等同于表锁。
还要把普通 SELECT 与锁定读分开。默认 RR 下的普通查询通常是一致性非锁定读,通过同一事务的读视图获得稳定结果;SELECT ... FOR UPDATE、SELECT ... FOR SHARE、UPDATE 和 DELETE 才进入锁定访问路径。把两类读混在一起,是判断“为什么有时没有间隙锁”的常见误区。

缩小锁范围的修复动作
先补正确索引。让过滤条件沿选择性合适的索引缩小扫描范围,是最直接的改进。索引设计应服务真实谓词和排序,不要为了“消除间隙锁”盲目创建重复索引。
让条件尽量精确。能够用主键或完整唯一键更新单条记录时,不要先宽范围查询再逐条修改。对于批处理,可按稳定主键分段并及时提交,减少一次事务覆盖的索引区间和持锁时间。
缩短事务生命周期。不要在事务内等待网络调用、人工确认或长时间计算。应用异常路径必须回滚,连接池归还连接前应确保事务已经结束。
按业务一致性要求选择隔离级别。READ COMMITTED 能减少搜索和索引扫描中的间隙锁,但允许并发插入形成新的范围行,并且每次一致性读都会建立新快照。它不是通用“解锁开关”,只能在业务接受相应语义后采用。
| 场景 | 典型锁边界 | 优先检查 |
|---|---|---|
| RR + 唯一索引完整等值 | 通常是目标索引记录 | 条件是否覆盖完整唯一键 |
| RR + 普通索引范围 | 扫描记录及相关间隙 | 执行计划与扫描区间 |
| RR + 缺少合适索引 | 可能扩大到大量聚簇索引记录与间隙 | 是否需要复合索引 |
| RC + 搜索或索引扫描 | 主要锁记录,通常不锁搜索间隙 | 外键、重复键检查等例外 |
把防复发检查写进上线清单
复查时不要只盯着 SQL 文本。先用 EXPLAIN 确认访问路径,再在演练环境用两个短事务复现冲突,并通过 Performance Schema 查看持有锁和等待关系。MySQL 8.4 中,performance_schema.data_locks 记录锁信息,data_lock_waits 用来关联阻塞锁与请求锁。
-- 查看当前 InnoDB 锁的对象、索引、模式和锁数据
SELECT ENGINE_TRANSACTION_ID,
OBJECT_SCHEMA,
OBJECT_NAME,
INDEX_NAME,
LOCK_TYPE,
LOCK_MODE,
LOCK_STATUS,
LOCK_DATA
FROM performance_schema.data_locks
WHERE OBJECT_SCHEMA = DATABASE()
AND OBJECT_NAME = 'task_queue';
-- 查看谁正在阻塞谁,便于把等待事务和持锁事务对应起来
SELECT REQUESTING_ENGINE_TRANSACTION_ID,
BLOCKING_ENGINE_TRANSACTION_ID,
REQUESTING_ENGINE_LOCK_ID,
BLOCKING_ENGINE_LOCK_ID
FROM performance_schema.data_lock_waits;
上线前至少回答四个问题:范围条件走哪个索引;最坏情况下扫描多少索引记录;事务最长持锁多久;区间内插入是必须阻止还是可以接受。把这些答案写进变更说明,比记住“RR 有间隙锁”更能减少下一次并发故障。
常见疑问
没有命中任何行,还会有间隙锁吗?
可能会。锁定范围搜索即使没有找到现有记录,也可能保护搜索落点附近的索引间隙,从而阻止其他事务插入会落入该范围的新记录。
普通 SELECT 为什么重复执行也看不到新插入行?
默认 RR 下,普通 SELECT 通常使用一致性非锁定读,同一事务读取既有快照。结果稳定来自 MVCC 读视图,不应据此推断查询拿到了间隙锁。
改成 READ COMMITTED 就一定不会等待吗?
不会。RC 主要减少搜索和索引扫描的间隙锁,记录锁冲突、唯一键冲突、外键检查和长事务仍可造成等待。隔离级别只能改变一部分锁策略,不能替代索引和事务治理。
如何确认锁住的是哪个索引?
先用 EXPLAIN 查看计划中的索引,再结合 performance_schema.data_locks 的 INDEX_NAME、LOCK_MODE 和 LOCK_DATA 观察。两者结合,才能把业务条件、扫描路径和实际锁对象对应起来。
参考资料:MySQL 8.4 Reference Manual:https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html、https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html、https://dev.mysql.com/doc/refman/8.4/en/innodb-information-schema-understanding-innodb-locking.html
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习