mvcc仅解决快照读下的幻读,对当前读(如select ... for update、update、delete)无效;其原理是事务启动时生成固定readview,后续快照读均复用,新插入行因db_trx_id > readview.max_trx_id而不可见。

MVCC只解决快照读场景下的幻读
MVCC 在 RR 隔离级别下,**根本不管当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)**,它只对普通 SELECT 生效。所谓“解决幻读”,仅指:事务 A 启动后第一次执行 SELECT 时生成一个 ReadView,之后所有普通查询都复用这个视图;即使事务 B 插入并提交了新行,事务 A 的后续 SELECT 依然看不到——这不是锁的功劳,是 MVCC 的版本可见性判断在起作用。
关键点在于:ReadView 中的 min_trx_id 和 max_trx_id 固化了事务可见范围,新插入行的 DB_TRX_ID 必然大于该视图的 max_trx_id,直接被过滤掉。
为什么你执行 SELECT ... FOR UPDATE 还能看到新插入的行?
因为这是当前读,不走 MVCC,也不用 ReadView。它会跳过快照,直接读取最新版本,并触发临键锁(Next-Key Lock)。但临键锁的覆盖范围取决于索引结构:
- 如果查询条件命中唯一索引(如主键),
Next-Key Lock只锁住对应记录 + 前隙,无法阻止其他间隙插入 - 如果查询条件走的是普通二级索引或无索引,InnoDB 会退化为全表扫描 + 行锁 + 间隙锁组合,但间隙锁定仍可能有漏网之鱼
- 最典型的漏点:
SELECT * FROM t WHERE age BETWEEN 20 AND 30 FOR UPDATE,若原数据中没有age=27,且 (25,28) 这个间隙未被完全覆盖,事务 B 就能成功插入age=27
UPDATE 或 DELETE 语句执行后,再 SELECT 却看到新行?
这不是 MVCC 失效,而是你混合了快照读和当前读:
- 事务 A 执行
UPDATE ... WHERE id > 3→ 当前读,读最新版,加Next-Key Lock - 事务 B 插入
id=11并提交 → 新行可见于后续当前读 - 事务 A 再执行普通
SELECT * FROM t WHERE id > 3→ 仍是快照读,仍用最初ReadView,看不到id=11 - 但如果事务 A 此时又执行一次
SELECT ... FOR UPDATE→ 当前读,就会看到id=11,甚至可能因锁冲突报Deadlock found when trying to get lock
这种“先更新后查到新行”的现象,本质是开发者误以为事务内所有读都自动同步,忽略了快照读与当前读的语义割裂。
真正容易被忽略的细节
RR 级别下,MVCC 和锁不是协同工作的“一套方案”,而是两套独立机制,分别服务于不同读类型。你写的每一条 SQL,必须明确它是快照读还是当前读——这由语法决定,不由事务隔离级别自动切换。很多线上死锁和幻读感知,根源不在配置,而在开发时没意识到 SELECT 和 SELECT ... FOR UPDATE 的底层行为天差地别。











