mysql 8.0 rr级别下普通select不加锁,靠mvcc快照读避免幻读;当前读(如for update)才触发next-key lock防幻读,但需满足范围查询+索引条件。

RR级别下普通SELECT不触发间隙锁,但当前读会
很多人误以为“RR默认防幻读=所有SELECT都加锁”,其实不是。普通 SELECT 是快照读,走MVCC,根本不会碰间隙锁;只有 SELECT ... FOR UPDATE、UPDATE、DELETE 这类当前读才会触发行锁+间隙锁组合的 Next-Key Lock。幻读是否发生,取决于你用的是哪种读——快照读靠一致性视图挡幻读,当前读靠间隙锁堵插入。
间隙锁只在范围查询+索引条件下生效
间隙锁不是独立存在的,它永远依附于 Next-Key Lock,而后者只在满足两个前提时才被InnoDB自动加上:
- 查询条件是范围(如
WHERE age > 25、WHERE name BETWEEN 'A' AND 'Z'),不是等值(WHERE id = 100只加Record Lock) - 该条件能走索引(主键、唯一索引、非唯一索引均可),如果
WHERE status = 'active'没索引,就会全表扫描并给每个间隙加锁,效果接近锁表 - 隔离级别必须是
REPEATABLE READ(READ COMMITTED下间隙锁完全不启用)
Insert Intention Lock才是间隙锁的实际拦截点
新行插入不是直接撞上间隙锁,而是先申请一个 Insert Intention Lock——它是一种特殊的意向锁,和已有的 Gap Lock 或 Next-Key Lock 冲突。比如:
事务A执行 SELECT * FROM t WHERE val BETWEEN 10 AND 20 FOR UPDATE,InnoDB在B+树中定位到现有记录后,实际加锁区间可能是 (5, 12] 和 (12, 25];事务B想插 val = 15,就要申请 (12, 25) 上的 Insert Intention Lock,与A持有的间隙部分冲突,于是阻塞。
这个机制决定了:间隙锁的边界由索引结构决定,不是SQL写的范围;插 val = 30 就不会被阻塞,哪怕你写的是 BETWEEN 10 AND 20。
RC级别下幻读重现,不是锁失效,是压根没加间隙锁
把隔离级别调成 READ COMMITTED 后出现幻读,常见误解是“锁丢了”。真相是:RC下InnoDB根本不会加间隙锁,只对实际命中的记录加 Record Lock。比如:
事务A执行 SELECT * FROM t WHERE age > 25 FOR UPDATE,只锁住 age = 26, 28, 30 这几行;事务B随时能 INSERT INTO t (age) VALUES (27),因为 (26, 28) 这个间隙在RC下完全开放。
验证当前级别用 SELECT @@transaction_isolation,别信客户端默认配置——很多ORM或连接池会悄悄覆盖它。
真正容易被忽略的是:间隙锁的存在感极低,它不锁数据、不阻塞读、甚至不显式出现在 INFORMATION_SCHEMA.INNODB_TRX 里;你只能通过插入阻塞、死锁日志或 SHOW ENGINE INNODB STATUS 中的 lock_mode gap 行去反推它是否存在。











