直接上结论:Gap Lock(间隙锁)并非在所有场景下都能生效,它对运行环境有着严格的要求——事务隔离级别必须设置为REPEATABLE READ,否则它根本没有“出场”机会。更准确地说,间隙锁从来不会单独存在,而是作为Next-Key Lock(临键锁)的组成部分,专门用来锁定那些“尚未存在的值”,根本目的是防止幻读现象。并且,它仅在范围查询使用了索引时才会真正被激活。
下面将几个关键点逐一拆解,帮助您深入理解MySQL间隙锁的工作机制。
Gap Lock仅在RR隔离级别下生效,RC下完全不触发
这是第一道硬性门槛。换句话说,如果您将隔离级别设为READ COMMITTED,即便执行SELECT * FROM t WHERE id > 10 FOR UPDATE,InnoDB也只会对已存在的行加Record Lock(行锁),不会对行之间的间隙施加任何锁。后果就是:幻读风险完全暴露——其他事务随时可以在id=15的位置插入一条新记录。
因此,在使用间隙锁之前,务必确认当前会话处于REPEATABLE READ隔离级别。检查方法很简单:运行SELECT @@transaction_isolation;,如果返回REPEATABLE-READ,才算满足前提条件。否则,Gap Lock是无法启用的。
Gap Lock从不单独存在,它总是Next-Key Lock的一部分
在InnoDB的锁机制中,您看不到“只加Gap Lock”的操作。间隙锁始终与Record Lock捆绑在一起,共同构成一个完整的Next-Key Lock。举个例子:假设表中有id=5、10、20三行数据,您执行SELECT * FROM t WHERE id > 5 FOR UPDATE,InnoDB实际会加上两个Next-Key Lock:(5,10](锁定记录10以及间隙(5,10))和(10,20](锁定记录20以及间隙(10,20)),并且还会添加一个(20,+supremum]。注意,括号中的开区间才是Gap Lock真正发挥作用的区域——它的职责是阻止其他事务申请Insert Intention Lock(插入意向锁)。要知道,任何插入操作必须先获取该意向锁才能继续,Gap Lock拦截的就是这一步。
这可能听起来有些反直觉,但恰恰是InnoDB的设计哲学:它锁定的是“位置”,而非具体的“值”。
无索引或等值查询时,Gap Lock不会启动
接下来列举几个常见但容易踩坑的场景:
WHERE status = 'pending':如果status字段没有建立索引,InnoDB会执行全表扫描,间隙锁根本不会参与。即使有索引,等值查询配合非唯一索引时,仍有可能只加Record Lock,而不覆盖前后间隙。WHERE id > 10(id为主键):这属于典型的走索引范围查询,条件满足,Next-Key Lock会生效,锁定(10,+supremum]整个后缀区间。SELECT * FROM t FOR UPDATE(无WHERE条件):这是最极端的情况——全表扫描时,InnoDB会锁定从(−∞, min_key]到(max_key, +supremum]的所有间隙,逻辑上相当于锁住了整张表。虽然操作本身允许,但并发性能会急剧下降。
验证Gap Lock是否真正生效,不要只看隔离级别
仅仅执行SET TRANSACTION ISOLATION LEVEL REPEATABLE READ是不够的,必须观察锁的实际行为。最可靠的验证方式是通过performance_schema.data_locks查询:SELECT LOCK_MODE, LOCK_DATA FROM performance_schema.data_locks WHERE LOCK_TRX_ID = '你的事务ID';。如果LOCK_MODE显示为RECORD & GAP或RECORD & NEXT-KEY,说明间隙部分已生效;如果只显示RECORD,则仅是行锁;如果根本查不到记录,很可能没有加锁。
还有一种更直观的验证方法:事务A执行SELECT * FROM t WHERE id > 5 FOR UPDATE后,让事务B立刻执行INSERT INTO t VALUES (6, ...)。如果该插入被阻塞,就能确认间隙确实被锁住了。
Gap Lock真正令人头疼的地方在于,它锁定的是“不存在的值”——多个事务对同一段间隙加锁时,彼此之间互不可见。正因如此,一旦插入的顺序不同,就可能引发死锁。更隐蔽的一点是:间隙锁只对当前读(FOR UPDATE或LOCK IN SHARE MODE)有反应;普通的SELECT走的是MVCC快照读,根本感知不到间隙锁的存在。换句话说,您用快照读查到的数据看起来没问题,但其他事务可能已经悄然在间隙中插入了新记录。
