MVCC 在 MySQL InnoDB 的实现
详解——MVCC 在MySQL InnoDB 的实现
目录
第一次看 MVCC,容易卡在一堆词上:undo log、Read View、隐藏字段、可见性、当前读、快照读。词都认识,连起来就容易乱。问题通常不在概念太难,而在于资料喜欢把好几层东西揉在一起讲。
本次只整理了 MySQL InnoDB。因为在大多数业务系统里,真正落地碰到的不是抽象的 MVCC,而是 InnoDB 这一套事务语义。之后,再看 Spring @Transactional、锁等待、重复读、长事务。
概括
MVCC 不是“给每行加个版本号”这么简单,但可以先这样记:InnoDB 会保留一条记录的历史版本线索,普通 select 不是直接读当前最新值,而是按当前事务的可见性规则,找到自己应该看到的那个版本。
这里有三样东西最重要:
- 当前记录上的隐藏字段
undo log里的旧版本信息- 一次普通查询创建出来的
Read View
这三样东西是分工关系,不是一件东西的三个名字。

先看当前这行记录。它可以近似理解成下面这样:
id = 1
amount = 300
DB_TRX_ID = 13
DB_ROLL_PTR = undo#72
DB_TRX_ID 表示“当前这一版,最后是谁改出来的”。你先把它理解成事务内部编号就行。DB_ROLL_PTR 表示“上一版去哪里找”。它指向的是一条 undo log 记录,不是 redo log。
所以 InnoDB 的版本不是靠一个单独的 version 字段管理的,而是靠“当前记录 + 回滚指针 + undo 链”管理的。当前记录放最新值,undo log 里保存更旧的值,顺着指针就能一路往前追。
普通 select 到底在干什么
普通 select 在 InnoDB 的 RC 和 RR 下,默认走的是快照读。它不先抢当前记录的锁,也不是一上来就只读最新值。它会先拿一份 Read View,再判断当前版本对自己可不可见。
执行逻辑可以压成四步:
- 创建或复用当前事务要用的
Read View - 先看当前记录上的
DB_TRX_ID是否可见 - 可见就直接返回当前版本
- 不可见就顺着
DB_ROLL_PTR去undo log找更老版本,直到找到第一个可见版本
这里最容易误会的一点是:Read View 不是“快照结果集”,它不是把查询结果整包存起来。它更像一套判断规则,用来决定你这次普通读取允许看到哪些事务产生出来的版本。

普通 select、update、select ... for update 经常被放在一起讲,但底层目标完全不同。
普通 select 的目标是“找到当前事务应该看到的版本”,所以它走快照读,要用 Read View。
update 和 select ... for update 的目标是“处理当前最新记录,并进入锁控制”。它们走的是当前读,不靠 Read View 去选历史版本。如果目标记录正被别的事务改着而且还没提交,它们就会等。
所以很多业务里看到的现象其实很正常:
- 普通
select不容易被并发写堵住 update容易遇到锁等待select ... for update会等待别的事务释放锁
这不是 MVCC 失效,而是三类 SQL 走的就不是同一条路径。
RR 和 RC 的差别,到底差在哪
很多文章会把 RR 和 RC 讲得很抽象。换成业务开发能直接用的话,就是下面这一句:
RR:同一事务里的普通select,通常复用第一次普通读取创建的Read ViewRC:每次普通select,都重新创建新的Read View
注意,这里说的是普通 select。不是说查询用 RR,增删改用 RC。隔离级别是事务级设置,不是按 SQL 类型切换。

如果放进同一个事务里看,差别会非常直接。
在 RR 下,第一次普通 select 建好视图,后面同事务里的普通 select 继续用这份视图,所以前后看到的数据范围更稳定。
在 RC 下,每次普通 select 都新建一份视图,所以你第二次查的时候,别人刚提交的新数据就可能已经进来了。
这也是为什么很多人会说:RR 更像事务级稳定读取,RC 更像语句级稳定读取。这个说法不算完整定义,但对理解日常业务现象很有帮助。
那 undo log 会不会一直堆到最早版本
不会。
一行记录持续更新,确实会不断生成新的 undo 记录,也确实会形成一条越来越长的历史链。但“链会变长”和“旧版本永远删不掉”不是一回事。
旧版本能不能清理,不看这行以后还会不会继续更新,而看当前系统里还有没有活跃事务可能读到这些旧版本。
只要满足两个条件,后台清理线程就可以逐步回收旧版本:
- 这些旧版本对应的事务已经结束,不再需要回滚
- 没有任何活跃的
Read View还可能访问到它们
所以真正导致 undo 堆积的,通常不是热点更新本身,而是长事务、长查询、长时间不提交的读事务。这类问题在线上比“单行频繁更新”更值得警惕。
跟 Spring @Transactional 怎么对上
很多人会说“同一个方法里两次查询结果一样,两个方法各查一次结果可能不一样”,这个方向没错,但前提要补全。
更准确的说法是:
- 两次查询要处在同一个真实事务里
- 隔离级别要看是
RR还是RC - 两次都得是普通
select,也就是快照读
如果这些条件成立,那么:
RR下,同一事务里的普通select通常复用同一份Read ViewRC下,同一事务里的普通select每次重新建Read View
如果中间掺了 update、select ... for update,那又是另一条路径,因为它们处理的是当前最新记录,不是普通快照读。
总结
DB_TRX_ID:当前这一版记录最后由哪个事务改出来DB_ROLL_PTR:上一版记录去哪找undo log:旧值存放位置,用于回滚和快照读回溯Read View:普通select的可见性规则- 普通
select:快照读,读自己应该看到的版本 update/select ... for update:当前读,处理当前最新记录并参与加锁RR:普通select复用事务里的快照RC:普通select每次生成新的快照
更多推荐




所有评论(0)