mvcc是innodb在rc和rr隔离级别下默认启用的底层机制,普通select走快照读(基于readview读历史版本),而select...for update等当前读则绕过mvcc读最新数据并加锁。

MVCC 不是某种开关或配置项,它是 InnoDB 引擎在 READ COMMITTED 和 REPEATABLE READ 隔离级别下默认启用的底层机制——你不用手动开启,只要用的是 InnoDB + 这两个隔离级别,它就在工作。
快照读 vs 当前读:先分清你读的是什么
很多人调了半天隔离级别却没效果,根本原因是没意识到:不是所有 SELECT 都走 MVCC。
- 普通
SELECT(无锁)→ 快照读 → 走 MVCC,读历史版本,不加锁 -
SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE、DELETE→ 当前读 → 绕过 MVCC,直接读最新行并加锁
如果你在事务里执行了 UPDATE 后又做普通 SELECT,后者仍可能看到旧值(RR 级别下),不是 bug,是 MVCC 正常行为——因为快照读基于事务启动时生成的 Read View,而当前读强制穿透到最新数据。
DB_TRX_ID、DB_ROLL_PTR、Undo Log:版本链怎么串起来的
每一行真实数据后面都藏着三个隐藏字段,MVCC 就靠它们定位“哪个版本该被谁看见”:
-
DB_TRX_ID:记录最后一次修改这行的事务 ID;插入/更新/标记删除都会更新它 -
DB_ROLL_PTR:指向 Undo Log 中上一个版本的指针;多个修改会形成一条链表 - Undo Log 本身存的是“改之前的样子”,不是 SQL 日志,而是物理前镜像,供回滚和版本回溯用
举个例子:事务 101 插入一行,DB_TRX_ID=101;事务 102 更新它,InnoDB 会把原值写进 Undo Log,新行的 DB_TRX_ID=102,DB_ROLL_PTR 指向那个 Undo Log 记录。事务 103 查询时,就顺着这条链往回找符合自己 Read View 规则的版本。
Read View 判断可见性:为什么 RR 级别能可重复读
Read View 是事务“看世界”的滤镜,关键字段有四个:m_ids(当时活跃事务 ID 列表)、min_trx_id、max_trx_id、creator_trx_id。判断某行版本是否可见,只看它的 DB_TRX_ID:
- 若
DB_TRX_ID → 已提交且早于本事务开始 → 可见 - 若
DB_TRX_ID >= max_trx_id→ 属于未来事务 → 不可见 - 若
min_trx_id → 再查 <code>DB_TRX_ID是否在m_ids中:在 = 未提交 → 不可见;不在 = 已提交 → 可见
区别就在这里:READ COMMITTED 每次语句执行前都新建 Read View;REPEATABLE READ 在事务第一次快照读时建一次,之后全用它——所以同一事务内多次 SELECT 结果一致,哪怕其他事务已提交新数据。
它不解决什么:幻读、更新丢失、串行化退化
MVCC 很强,但不是万能的:
- 幻读依然存在:
REPEATABLE READ下,SELECT看不到新插入行(靠间隙锁补救),但INSERT ... SELECT或UPDATE ... WHERE仍可能读到新行导致幻象 - 更新丢失不防:两个事务并发读-改-写同一行,后提交者覆盖前提交者,MVCC 不介入校验——得靠应用层加
version字段或重试逻辑 - 串行化(
SERIALIZABLE)下,普通SELECT会自动转成SELECT ... LOCK IN SHARE MODE,MVCC 失效,回归锁模式
真正容易被忽略的是:MVCC 的“多版本”全靠 Undo Log 维持,而 Undo Log 不会无限保留。如果长事务一直不提交,它创建的 Read View 会让所有更早的版本都无法清理,undo log 文件持续膨胀,最终可能触发 ERROR 1205 (HY000): Deadlock found when trying to get lock 或磁盘满故障。











