xancel的自留地
文章
技术

深入理解MySQL MVCC

2026年3月15日(更新于 2026年3月17日) 3 分钟阅读 浏览 5 喜欢 0 评论 0
AI 摘要

MVCC(多版本并发控制)是InnoDB实现一致性读的核心机制,主要用于解决读写并发冲突,让读操作尽量不阻塞写操作。它通过隐藏字段、undo log版本链和Read View三个核心组件,结合可见性判断算法,为快照读提供数据版本可见性。RC和RR隔离级别的区别在于Read View的创建时机不同,RC每次快照读都新建Read View,而RR通常复用第一次快照读创建的Read View,从而实现可重复读。

首先,什么是 MVCC

MVCC 的中文是多版本并发控制,它是 InnoDB 在处理普通 SELECT 这类快照读时实现一致性读的核心机制,主要服务于读已提交(RC)可重复读(RR) 这两个隔离级别。

并发事务里常见的冲突主要有两类:

  • 读写冲突:事务 A 在读一行数据,事务 B 同时修改这行数据
  • 写写冲突:两个事务同时修改同一行数据

写写冲突仍然需要依赖锁来保证串行化;MVCC 主要解决的是读写并发时,如何让读操作尽量不阻塞写操作。

需要注意,MVCC 主要用于快照读,不适用于所有读操作。像 SELECT ... FOR UPDATESELECT ... FOR SHAREUPDATEDELETE 这类当前读,仍然要结合锁来工作。

MVCC 的三个核心点

  1. 隐藏字段

    • trx_id:最后一次修改该记录版本的事务 ID
    • roll_pointer:指向该记录上一个历史版本的 undo log 指针
  2. undo log 版本链

    一行记录每次被修改时,旧版本不会立刻消失,而是被写入 undo log,再通过 roll_pointer 串起来,形成一条从新到旧的版本链。

  3. Read View

    Read View 可以理解为事务做快照读时拿到的一份“可见性规则”。事务会基于这份规则,从版本链上判断哪个版本对自己可见。

Read View 中几个关键字段

  • m_ids:创建 Read View 时,系统中活跃的读写事务 ID 集合
  • min_trx_idm_ids 中最小的事务 ID
  • max_trx_id:创建 Read View 时,系统将要分配给下一个事务的 ID
  • creator_trx_id:创建这个 Read View 的事务 ID

这里要特别注意:

  • 记录上的 trx_id,表示“这个版本是由哪个事务生成的”
  • creator_trx_id,表示“当前正在读取数据的事务是谁”

这两个不是同一个概念。

可见性判断算法

当事务进行快照读时,会从版本链的最新版本开始判断可见性。这里参与判断的 trx_id,指的是“当前正在检查的那个记录版本的事务 ID”,不是当前读请求所属事务的 ID。

判断规则可以概括为:

  1. 如果版本的 trx_id == creator_trx_id该版本可见

    原因:这是当前事务自己生成的版本,当前事务当然可以看到。

  2. 如果版本的 trx_id < min_trx_id该版本可见

    原因:说明生成这个版本的事务,在 Read View 创建前就已经提交了。

  3. 如果版本的 trx_id >= max_trx_id该版本不可见

    原因:说明这个版本对应的事务,是在 Read View 创建之后才开始的。

  4. 如果 min_trx_id <= trx_id < max_trx_id

    • trx_idm_ids 中:不可见,说明该事务在创建 Read View 时仍然活跃,尚未提交
    • trx_id 不在 m_ids 中:可见,说明该事务在创建 Read View 前已经提交

如果某个版本不可见,就顺着版本链继续找更老的版本,直到找到第一个可见版本为止。

RC 和 RR 的本质区别

  1. RC(读已提交)

    每次执行快照读时,都会创建新的 Read View,所以同一个事务里两次普通 SELECT,可能看到不同的已提交结果,这就是不可重复读产生的原因。

  2. RR(可重复读)

    默认情况下,事务中的第一次快照读会创建 Read View,后续快照读复用同一个 Read View,因此同一个事务里的多次普通 SELECT 通常会看到同一份快照。

这里的表述要稍微严谨一点:

  • RR 下“保持一致”的是普通快照读看到的历史快照
  • 如果当前事务自己更新了数据,那么它仍然可以看到自己更新后的结果
  • 如果执行的是当前读,也不走这套快照可见性逻辑

另外,InnoDB 中 RR 并不是“事务一启动就一定创建 Read View”。通常是第一次执行快照读时才创建;如果显式使用了 START TRANSACTION WITH CONSISTENT SNAPSHOT,则会在事务开始时创建一致性视图。

举例说明版本选择过程

假设有三个事务并发执行:

  • 事务 101:较早启动,修改了某行数据,但还没有提交
  • 事务 102:稍后修改了同一行数据,并且已经提交
  • 事务 103:当前事务,正在执行一次快照读

假设这行数据原本的最后已提交版本是 trx_id=100

之后:

  • 事务 102 修改该行,生成一个新版本,trx_id=102
  • 事务 101 又基于更新后的数据生成更“新”的版本,trx_id=101,但它尚未提交

于是版本链可以表示为:

(101) -> (102) -> (100)

其中 (101) 是当前记录上最新的版本。

现在事务 103 在执行第一次快照读时创建 Read View。此时:

  • 活跃事务只有 101,所以 m_ids = {101}
  • min_trx_id = 101
  • max_trx_id = 104,表示下一个将被分配的事务 ID 是 104
  • creator_trx_id = 103

事务 103 读取这行数据时,会这样判断:

  1. 先看最新版本 (101)

    trx_id=101 落在 min_trx_id <= trx_id < max_trx_id 区间内,并且 101 在 m_ids 中,说明该版本对应的事务在创建 Read View 时仍然活跃,因此不可见。

  2. 顺着版本链看下一个版本 (102)

    trx_id=102 也还是落在min_trx_id <= trx_id < max_trx_id 区间内,但 102 不在 m_ids 中,说明它在 Read View 创建前已经提交,因此这个版本可见。

  3. 返回版本 (102) 的数据

所以,事务 103 读到的是事务 102 提交后的值,而不是事务 101 尚未提交的值。

如果隔离级别是 RC,那么事务 103 每次执行快照读都会重新创建 Read View:

  • 只要事务 101 还没提交,新的 Read View 中仍然会把 101 视为活跃事务,因此看不到版本 101
  • 一旦事务 101 提交,下一次快照读重新生成 Read View 时,101 不再出现在活跃事务列表中,此时就可能读到版本 101
www.bilibili.com

10分钟带你深刻理解MySQL中的MVCC机制

参考引用
www.bilibili.com

MySQL数据库入门到精通

参考引用
喜欢 0
评论区在赶来的路上...