快照读不访问数据页,而是基于ReadView沿undo log版本链比对trx_id找可见版本;RR复用首个ReadView,RC每次SELECT新建ReadView。

快照读不查数据页,只走 MVCC 版本链
快照读执行时根本不会访问聚簇索引的数据页,而是直接根据事务启动时生成的 ReadView,沿着记录的 roll_pointer 遍历 undo log 中的历史版本链,逐个比对 trx_id 是否可见。只要找到一个满足可见性规则的版本,就立刻返回,不加任何锁。
常见错误现象:在 RR 级别下,事务 A 执行两次 SELECT * FROM t WHERE id = 1,第二次仍看到旧值,不是引擎“没刷新”,而是它压根没去查最新数据页——它只认自己那个 ReadView 里能看见的版本。
- RC 级别每次
SELECT都新建ReadView,所以可能两次读到不同已提交值 - RR 级别整个事务复用首个
ReadView,哪怕其他事务已提交新行,也看不到 - 即使
WHERE条件命中唯一索引,快照读也绝不会触发gap lock或next-key lock
当前读必须定位最新行并立即加锁
当前读(如 SELECT ... FOR UPDATE、UPDATE、DELETE)会跳过 MVCC 版本链,直接定位到聚簇索引上最新已提交的那条物理记录,然后按语义加锁:X 锁或 S 锁,并可能延伸出间隙锁。
典型陷阱:执行 UPDATE t SET v = v + 1 WHERE id > 100,InnoDB 先做当前读扫描满足条件的所有最新行,再对每行加 X 锁;若 id > 100 是范围且无索引,还会锁住整个区间,导致严重阻塞。
-
SELECT ... FOR UPDATE→ 加 X 锁,阻塞其他事务的当前读和写 -
SELECT ... LOCK IN SHARE MODE→ 加 S 锁,允许并发读,但阻塞 X 锁 -
INSERT ... ON DUPLICATE KEY UPDATE→ 对冲突的唯一键行做当前读并加 X 锁,是死锁高发点
隔离级别只影响快照读的 ReadView 复用策略
很多人误以为 RR 和 RC 的区别在于“是否加锁”,其实不然:当前读在两种级别下都强制加锁,行为完全一致;真正差异只发生在快照读路径——RR 复用初始 ReadView,RC 每次 SELECT 都重建。
这意味着:你在 RR 下执行 SELECT 看不到新提交数据,不是因为“锁没释放”,而是因为 MVCC 机制主动屏蔽了它;而你在 RC 下看到新值,也不是因为“锁变松了”,只是 ReadView 更新了。
- RR 下首次
SELECT后,后续所有快照读都绑定同一个ReadView,包括子查询、视图展开 - RC 下每个快照读独立判断可见性,可能同一事务内两次读结果不同
- 无论 RR 还是 RC,
UPDATE或SELECT FOR UPDATE都会读到最新已提交值并加锁
容易被忽略的临界点:快照读也可能“卡住”
快照读本身不加锁,但它依赖的 undo log 不能被 purge —— 如果长事务一直不提交,其 ReadView 会阻止 purge 线程清理旧版本,导致 undo 表空间暴涨、历史版本链变长,最终拖慢所有快照读性能。
更隐蔽的是:当某行正在被当前读加锁(比如被 SELECT FOR UPDATE 占着),快照读虽然不阻塞,但若它需要遍历的某个旧版本恰好被 purge 锁等待,也可能间接延迟返回。
所以“无锁”不等于“零成本”,MVCC 的代价藏在 undo 管理和版本链遍历里,尤其在高更新+长事务混合场景下,问题才真正浮现。











