幻读是事务中两次范围查询结果行数不一致,因其他事务插入/删除导致;不可重复读是同一行数据值被修改。MySQL的REPEATABLE READ通过间隙锁抑制幻读但未消除其语义,SERIALIZABLE虽彻底解决却牺牲并发,合理索引与SQL优化更实用。

幻读到底是什么,和不可重复读有什么区别
幻读不是“看到幻觉”,而是指事务中两次 SELECT 同一范围数据,第二次多出(或少掉)了新插入(或删除)的行——这些行在第一次查询时并不存在,但被别的事务提交了。关键在于:它针对的是「范围查询」,比如 WHERE age > 25 或 SELECT * FROM user WHERE status = 1,而不是单行主键查询。
不可重复读是查同一行,值变了;幻读是查一个范围,行数变了。很多人混淆,是因为 MySQL 默认的 REPEATABLE READ 隔离级别下,InnoDB 用间隙锁(Gap Lock)+ 行锁模拟出“无幻读”效果,但这只是表象——它没真正消除幻读语义,只是把并发写冲突提前拦住了。
MySQL 的 REPEATABLE READ 真的能防止幻读吗
在 InnoDB 中,REPEATABLE READ 下普通 SELECT 是快照读(Snapshot Read),不加锁,靠 MVCC 版本链避免脏读和不可重复读;但只要涉及当前读(如 SELECT ... FOR UPDATE、UPDATE、DELETE),InnoDB 就会加临键锁(Next-Key Lock),即「行锁 + 间隙锁」,从而封锁索引区间,阻止其他事务在该范围内插入新记录。
这意味着:
- 如果你只用纯
SELECT(无锁读),幻读不会发生——因为看到的是事务开始时的一致快照 - 但如果你执行
SELECT ... FOR UPDATE查范围,然后另一个事务尝试INSERT进这个间隙,会被阻塞或报死锁——这不是“防止幻读”,而是“用锁压制了幻读发生的条件” - 一旦你没走索引(比如
WHERE name LIKE '%abc%'),间隙锁退化为全表锁,性能雪崩
想真正规避幻读,必须用 SERIALIZABLE 吗
不用。MySQL 的 SERIALIZABLE 确实会对所有 SELECT 自动加上 LOCK IN SHARE MODE,变成串行执行,彻底杜绝幻读,但代价极高:并发能力几乎归零,且容易触发大量锁等待。
更务实的做法是:
- 确保范围查询字段有有效索引——间隙锁只在索引上生效,否则锁不住间隙
- 用
SELECT ... FOR UPDATE显式加锁时,尽量缩小范围,避免WHERE条件太宽泛 - 业务层配合:对“新增是否合法”做幂等校验,比如插入前先
SELECT COUNT(*)判断是否已存在同类逻辑约束,而不是依赖隔离级别兜底 - 必要时改用应用级分布式锁(如 Redis 锁住业务维度 key),比数据库锁更可控
为什么加了索引还是出现幻读
常见原因不是索引没建,而是用了非唯一索引 + 查询条件未命中索引最左前缀,导致优化器放弃使用间隙锁。例如:
CREATE INDEX idx_status ON user(status); -- 下面这句可能不走间隙锁: SELECT * FROM user WHERE status = 1 AND deleted = 0 FOR UPDATE;
如果 deleted 不在索引中,InnoDB 可能只对 status = 1 加间隙锁,而允许其他事务在相同 status 下插入 deleted = 1 的新行——这就是漏掉的“幻行”。
验证方式很简单:
- 执行
EXPLAIN确认是否走了预期索引 - 查
INFORMATION_SCHEMA.INNODB_TRX和INNODB_LOCKS(8.0+ 用performance_schema.data_locks)看实际加了什么锁 - 避免在
FOR UPDATE查询中混用函数、隐式类型转换、OR条件——它们都可能导致锁退化
幻读的本质不是数据库“bug”,而是隔离级别与并发控制策略之间的权衡结果;指望靠调高隔离级别一劳永逸,往往掩盖了索引设计、SQL 写法和业务逻辑耦合不深的问题。










