repeatable read 隔离级别不等于防幻读,是否防住取决于读写类型、索引使用及锁机制;快照读靠mvcc避免幻读,当前读依赖next-key lock,间隙锁失效常见于无索引、唯一等值查询或空范围条件。

MySQL 的 REPEATABLE READ 隔离级别本身不等于“防幻读”,它只提供机制基础;真正能否防住,取决于你**怎么查、查什么、有没有锁、锁在哪儿**。
快照读 vs 当前读:先分清你在读哪一种
同一事务里两次 SELECT 看到不同行数,未必是 bug —— 很可能第一次是快照读,第二次是当前读,或反过来。
- 快照读(普通
SELECT):靠 MVCC 生成的Read View,事务启动后所有快照读都看到相同版本数据,天然屏蔽新插入行 → 不会出现幻读 - 当前读(
SELECT ... FOR UPDATE、UPDATE、DELETE):绕过 MVCC,直接读最新已提交数据,并加next-key lock→ 这时才真正考验间隙锁是否生效 - 常见误判:事务 A 先执行快照读没看到新行,再执行
UPDATE ... WHERE age > 20却更新了新插入的行 —— 这不是幻读“没防住”,而是快照读和当前读混用导致的语义不一致
间隙锁失效的三个典型场景
即使开了 REPEATABLE READ,SELECT ... FOR UPDATE 也不一定锁住插入点。以下情况会让间隙锁形同虚设:
- WHERE 条件字段**没建索引**(如对
TEXT字段做LIKE '%abc'),InnoDB 回退为全表扫描,无法定位间隙 → 插入完全不受阻 - 查询走了索引,但条件是**等值且命中唯一索引**(如
WHERE id = 100),InnoDB 可能只加记录锁,不加间隙锁 → 其他事务仍可在(99, 100)或(100, 101)插入 - 范围条件写成空集(如
WHERE id > 999999且表中最大id是 1000),InnoDB 可能不加任何间隙锁 → 插入新高 ID 行不会被阻塞
让 next-key lock 真正起效的实操要点
要让间隙锁封锁插入区间,必须同时满足:
- 查询必须走 B+ 树索引:主键、唯一索引、普通二级索引都行,但**函数索引、前缀索引未覆盖完整条件时会失效**
- WHERE 必须是**范围条件**(如
id > 100、name BETWEEN 'a' AND 'z'),等值查询只锁单条记录及其前隙,不锁整个区间 - 必须用**当前读语句**:
SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE、DELETE - 示例有效写法:
START TRANSACTION; SELECT * FROM t WHERE status = 1 FOR UPDATE;
——前提是status有索引;若status是低基数字段(如只有 0/1),InnoDB 可能锁整段索引区间,需权衡并发影响
最容易被忽略的点:快照读后写入的语义断裂
这是线上最隐蔽的问题:事务先用普通 SELECT 判断某条件无数据(比如“用户未下单”),再执行 INSERT;但两个操作之间,另一个事务已插入并提交 —— 你的 INSERT 会成功,导致业务逻辑错乱。这不是隔离级别失效,而是应用层没把“读-写”当作原子操作。此时必须改用当前读(如 SELECT ... FOR UPDATE)提前占住间隙,或引入应用层唯一约束+重试。











