幻读指事务中范围查询返回不一致行数,因其他事务插入新行被后续查询感知;需满足RR或更低隔离级、范围查询、并发插入三条件;MVCC快照读不覆盖新插入行,故仍发生幻读。

幻读发生在事务中执行相同范围查询时,返回了前后不一致的行数,尤其是出现了“新插入的、本不该看到的记录”。它不是脏读或不可重复读,核心在于其他事务在当前事务的查询范围内插入了新行,并且这些插入被当前事务后续查询“感知”到了。
幻读形成的三个必要条件
只有同时满足以下三点,幻读才可能发生:
- 事务隔离级别为可重复读(REPEATABLE READ)或更低:在 MySQL 默认的 RR 级别下,普通 SELECT 不加锁,无法阻止其他事务插入;而 SERIALIZABLE 可通过间隙锁或串行化规避,但性能代价大。
- 执行的是范围查询(如 WHERE age BETWEEN 20 AND 30、WHERE id > 100):单值查询(如 WHERE id = 105)只锁定已有记录,不锁区间,无法防止新记录插入到“空隙”中。
- 其他事务在当前事务两次查询之间,向该范围插入了新行并提交:例如第一次查出 5 条,另一事务插入一条 age=25 的用户并提交,第二次查就变成 6 条——新增的这条就是“幻行”。
为什么 MVCC 在 RR 级别下仍可能发生幻读
MySQL 的 RR 级别基于 MVCC 实现快照读(普通 SELECT),它能看到事务启动时已提交的数据版本,但不保证未来不会出现新版本的行。MVCC 解决了不可重复读(同一行值变化),但对“新插入的行”,快照里本来就没有,所以第二次查询会自然读到新提交的行——这就是幻读的本质:读到了快照之外“新生”的记录。
注意:MySQL InnoDB 对当前读(如 SELECT ... FOR UPDATE、UPDATE、DELETE)会加临键锁(Next-Key Lock),即记录锁 + 间隙锁,能阻塞范围内的插入,从而避免当前读下的幻读;但快照读不受此保护。
典型场景还原:订单状态范围查询
假设业务需统计“待处理订单(status = 0)的数量”,并在统计后批量更新:
- T1 事务执行 SELECT COUNT(*) FROM orders WHERE status = 0; 得到 100 条;
- T2 事务插入一条新订单 INSERT INTO orders (..., status) VALUES (..., 0); COMMIT;;
- T1 再次执行同一条 COUNT 查询,得到 101 条;
- 若 T1 接着执行 UPDATE orders SET status = 1 WHERE status = 0;,实际会更新 101 行——其中 1 行是 T2 插入的,T1 并未在第一步“看见”它,却在更新时一并处理了。
这个过程体现了幻读带来的逻辑错位:统计与操作作用于不同数据集。
如何真正避免幻读
不能只依赖隔离级别,需结合具体读写类型选择策略:
- 用当前读替代快照读:将普通 SELECT 改为 SELECT ... FOR UPDATE 或 SELECT ... LOCK IN SHARE MODE,触发临键锁,阻止其他事务在查询范围内插入。
- 升级隔离级别到 SERIALIZABLE:MySQL 会自动将所有普通 SELECT 转为隐式加锁,但并发性能明显下降,一般不推荐作为首选。
- 应用层加唯一约束或预占机制:例如插入前先查是否存在,或用 INSERT ... ON DUPLICATE KEY UPDATE 避免重复插入;对范围敏感场景,可引入状态机或队列控制写入节奏。
- 明确区分快照读与当前读语义:在业务逻辑中,若“统计后立即操作”,应统一使用当前读,确保一致性视图。










