mvcc本身不避免幻读,仅保障快照读一致性;幻读靠next-key lock在当前读中抑制。普通select查不到新行是因readview固定可见范围,但select for update等当前读会读最新数据并可能因间隙插入导致两次结果不一致,故仍属幻读。

MVCC 本身不避免幻读,它只保证快照读的一致性;幻读靠 Next-Key Lock 抑制,且仅在当前读场景生效。
为什么“SELECT 不加锁”查不到新插入的行,却仍算幻读?
MVCC 的快照读(如普通 SELECT)基于事务启动时生成的 ReadView,该视图固定了可见事务 ID 范围(min_id~max_id)。后续插入若由新事务提交,其 trx_id 大于当前 ReadView.max_id,自然不可见——这看起来“没幻读”,但只是表象。
问题出在当前读:
-
SELECT ... FOR UPDATE或UPDATE ... WHERE必须读最新数据,触发 Next-Key Lock - 若另一事务在间隙中插入并提交,你再次执行相同条件的
SELECT ... FOR UPDATE,就会看到那条“新行” - 两次当前读结果不一致 → 幻读成立
REPEATABLE READ 下的幻读到底“解决”了没有?
MySQL 官方文档和 InnoDB 实现层面都明确:REPEATABLE READ 隔离级别 并未彻底消除幻读,而是通过组合手段大幅抑制:
- 快照读(普通
SELECT)用ReadView隔离,对范围查询“看不见”新行 → 表面无幻读 - 当前读(带锁或写操作)依赖 Next-Key Lock 锁住索引区间(记录 + 间隙),阻止并发插入 → 实际业务中多数幻读被拦住
- 但若应用混合使用快照读与当前读(例如先
SELECT判断存在性,再INSERT),仍可能因间隙未被锁住而出现幻读
哪些操作会意外触发当前读,导致幻读暴露?
很多看似“只读”的逻辑,底层会隐式执行当前读,绕过 MVCC 快照保护:
-
UPDATE ... WHERE id = ?执行前,InnoDB 先做一次当前读定位记录 -
INSERT ... ON DUPLICATE KEY UPDATE的冲突检测属于当前读 - Laravel 的
firstOrCreate()底层是SELECT+INSERT,高并发下SELECT是快照读,但INSERT可能因间隙锁冲突报死锁,或漏判导致重复插入 - 任何使用
ORDER BY ... LIMIT 1配合FOR UPDATE的分页更新,若索引非唯一,间隙范围难精确覆盖
真正可控的幻读防御策略是什么?
不依赖“默认隔离级别是否够用”,而是从访问模式入手控制确定性:
- 所有事务按相同顺序访问行:例如统一
SELECT ... ORDER BY id ASC FOR UPDATE预占锁 - 显式用
SELECT ... LOCK IN SHARE MODE或FOR UPDATE替代纯快照读,尤其在判断+写入链路中 - 避免在 REPEATABLE READ 下依赖“两次普通 SELECT 结果一致”来实现业务逻辑,因为这不是隔离承诺
- SERIALIZABLE 级别虽能杜绝幻读,但代价是全范围加锁,吞吐骤降,生产环境慎用
最易被忽略的一点:幻读不是数据“多出来”,而是**同一查询条件在不同读类型下行为分裂**。只要代码里混用快照读和当前读,又没对间隙做显式锁定,就始终留有缺口。











