幻读是mysql在可重复读隔离级别下同一事务多次范围查询结果行数不一致的现象,因其他事务插入符合where条件的新记录所致;其根源在于mvcc快照读不锁定未来插入、间隙锁在无索引时失效,且insert本身不加间隙锁;典型场景为事务a两次查询id>100,中间事务b插入id=105并提交,第二次查询多出一行;解决方案包括确保查询走索引以启用临键锁、显式当前读加锁、用唯一约束替代先查后插、应用层分布式锁串行化关键操作;不推荐直接升至serializable,因其将所有select转为加锁读,严重降低并发性能。

幻读是 MySQL 中一种特殊的并发问题,发生在可重复读(RR)隔离级别下,表现为同一个事务中多次执行相同范围的 SELECT 查询,结果集行数不一致——比如第二次查询“凭空多出”了几行记录。这并非数据被修改,而是其他事务插入了符合当前查询条件的新数据,并在本事务后续查询中被看到。
幻读产生的根本原因
MySQL 默认的 RR 隔离级别通过 MVCC 实现快照读(如普通 SELECT),能避免脏读和不可重复读,但 不能阻止其他事务插入新记录。这些新插入的行可能满足本事务之前查询的 WHERE 条件,当本事务再次执行相同查询(尤其是当前读,如 SELECT ... FOR UPDATE、UPDATE、DELETE)时,就会看到这些“幻影行”。
关键点在于:
- MVCC 只为事务启动时生成一个一致性视图(Read View),对已存在记录有效,但不锁定未来插入的记录;
- 间隙锁(Gap Lock)虽在 RR 下启用,但仅作用于索引区间,若查询条件未命中索引(或使用全表扫描),间隙锁失效,无法阻止插入;
- INSERT 操作本身不加间隙锁,而是由插入位置的前后索引项上的间隙锁“保护”,若无对应索引约束,插入就无阻碍。
如何确认是否发生幻读
典型场景包括:
phpweb1.0基于php+mysql+smarty开发的企业解决方案,总体感觉简洁快速,适合小型企业的建站方案,也适合初学者学习。 之前发布过phpweb1.0的原始版本,仅提供大家交流和学习,但很多的爱好者提出了一些不足和好评,本不想继续开发1.0,因为2.0已经开发完毕而且构架与1.0完全不同,但是有些使用者喜欢这种简洁和简便,应大家的要求,美化和优化了一些不足之处。后台更加简洁美观。
- 事务 A 执行
SELECT * FROM t WHERE id > 100;得到 5 行; - 事务 B 插入
INSERT INTO t VALUES (105, ...);并提交; - 事务 A 再次执行相同 SELECT(尤其带
FOR UPDATE),结果变为 6 行——这就是幻读。
注意:纯快照读(无锁 SELECT)在 RR 下不会看到新插入行,所以幻读通常出现在当前读操作中,是“可重复读”语义被打破的表现。
有效解决方案
解决幻读需从锁机制和设计层面入手,而非简单提升隔离级别:
- 确保查询走索引:在 WHERE 条件字段上建立合适索引,让 MySQL 能使用间隙锁(Gap Lock)或临键锁(Next-Key Lock)封锁插入区间;
-
显式加锁控制插入点:对范围查询使用
SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE,触发临键锁,封锁索引记录及其前隙; -
用唯一约束替代范围判断:例如用
INSERT ... ON DUPLICATE KEY UPDATE替代“先查后插”逻辑,从源头避免并发插入冲突; - 应用层串行化关键路径:对强一致性要求的操作(如生成单号、库存扣减),借助 Redis 分布式锁或数据库 select for update + 业务逻辑校验,保证同一条件只被一个事务处理。
为什么不用串行化(SERIALIZABLE)?
SERIALIZABLE 级别会将所有普通 SELECT 隐式转为 SELECT ... LOCK IN SHARE MODE,虽可彻底避免幻读,但大幅降低并发性能,易引发锁等待甚至死锁。实际项目中极少采用,应优先通过索引优化与合理加锁来精准防控。









