搞懂 InnoDB MVCC:一致性读、ReadView、undo log 原理详解

时间:2026-08-24 16:03:03 来源:幸福记忆 作者:远方 阅读:114

一、一致性非锁定读和锁定读

1.1 一致性非锁定读

    对于 一致性非锁定读(Consistent Nonlocking Reads)
的实现,通常做法是加一个版本号或者时间戳字段,在更新数据
的同时版本号 + 1 或者更新时间戳。查询时,将当前可见的版
本号与对应记录的版本号进行比对,如果记录的版本小于可见版
本,则表示该记录可见

   在mysql的innodb存储引擎中所实现的(MVCC多版本并发控制)
就是对非锁定读的实现。如果读取的行正在执行 DELETE 或 UPDATE
操作,这时读取操作不会去等待行上锁的释放。相反地,InnoDB 存
储引擎会去读取行的一个快照数据,对于这种读取历史数据的方式,
我们叫它快照读 (snapshot read)

   在 Repeatable Read 和 Read Committed 两个隔离级别下,
如果是执行普通的 select 语句(不包括 select ...
lock in share mode ,select ... for update)则会使用 一
致性非锁定读(MVCC)。并且在 Repeatable Read 下 MVCC
实现了可重复读和防止部分幻读

1.2 锁定读

如果执行的是下列语句,就是 锁定读

  • select ... lock in share mode

  • select ... for update

  • insert、update、delete 操作  


    在锁定读下,读取的是数据的最新版本,这种读也被称为
当前读(current read)。锁定读会对读取到的记录加锁
  • select ... lock in share mode:对记录加 S 锁,其它事务也可以加S锁,如果加 x 锁则会被阻塞

  • select ... for update、insert、update、delete:对记录加 X 锁,且其它事务不能加任何锁

在一致性非锁定读下,即使读取的记录已被其它事务加上 X 锁, 这时记录也是可以被读取的,即读取的快照数据。上面说了,在 Repeatable Read 下 MVCC 防止了部分幻读,这边的 “部分” 是指 在 一致性非锁定读 情况下,只能读取到第一次查询之前所插入的数 据(根据 Read View 判断数据可见性,Read View 在第一次查询时 生成)。但是!如果是 当前读 ,每次读取的都是最新数据,这时如 果两次查询中间有其它事务插入数据,就会产生幻读。所以, InnoDB 在实现Repeatable Read 时,如果执行的是当前读,则会对读取的 记录使用 Next-key Lock ,来防止其它事务在间隙间插入数据

二、InnoDB 对 MVCC 的实现

    MVCC 的实现依赖于:隐藏字段、Read View、undo log。在
 内部实现中,InnoDB 通过数据行的 DB_TRX_ID 和 Read View
 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR
 找到 undo log 中的历史版本。每个事务读到的数据版本可能
 是不一样的,在同一个事务中,用户只能看到该事务创建
 Read View 之前已经提交的修改和该事务本身做的修改

2.1 隐藏字段

在内部,InnoDB 存储引擎为每行数据添加了三个 隐藏字段  

字段说明
DB_TRX_ID(6字节)表示最后一次插入或更新该行的事务 id。此外,delete 操作在内部被视为更新,只不过会在记录头 Record header 中的 deleted_flag 字段将其标记为已删除
DB_ROLL_PTR(7字节)回滚指针,指向该行的 undo log 。如果该行未被更新,则为空
DB_ROW_ID(6字节)如果没有设置主键且该表没有唯一非空索引时,InnoDB 会使用该 id 来生成聚簇索引

2.2 ReadView

Read View主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务”  

字段说明
m_low_limit_id目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见
m_up_limit_id活跃事务列表 m_ids 中最小的事务 ID,如果 m_ids 为空,则 m_up_limit_id 为 m_low_limit_id。小于这个 ID 的数据版本均可见
m_idsRead View 创建时其他未提交的活跃事务 ID 列表。创建 Read View时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。m_ids 不包括当前事务自己和已提交的事务(正在内存中)
m_creator_trx_id创建该 Read View 的事务 ID

事务可见性示意:   事务可见性简单示意图

readview.png

2.3 再谈undo-log

2.3.1 作用

  • 当事务回滚时用于将数据恢复到修改前的样子

  • 另一个作用是 MVCC ,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过 undo log 读取之前的版本数据,以此实现非锁定读

2.3.2  undo log分类

在 InnoDB 存储引擎中 undo log 分为两种,
insert undo log 和 update undo log

insert undo log:

    在 insert 操作中产生的 undo log。因为 insert 操作的
记录只对事务本身可见,对其他事务不可见,故该 undo log
可以在事务提交后直接删除。不需要进行 purge 操作

update undo log:

    update 或 delete 操作中产生的 undo log。该 undo log可
能需要提供 MVCC 机制,因此不能在事务提交时就进行删除。提
交时放入 undo log 链表,等待 purge线程 进行最后的删除

第一次被修改时

change01.png

第二次被修改时

change02.png

不同事务或者相同事务对同一记录行的修改,会使该记录行的 undo log 成为一条链表,链首就是最新的记录,链尾就是最早的旧记录。

2.4 数据可见性算法

    在 InnoDB 存储引擎中,创建一个新事务后,执行每个 select
语句前,都会创建一个快照(Read View),快照中保存了当
前数据库系统中正处于活跃(没有 commit)的事务的 ID 号。
其实简单的说保存的是系统中当前不应该被本事务看到的其他事
务 ID 列表(即 m_ids)。当用户在这个事务中要读取某个记录
行的时候,InnoDB 会将该记录行的 DB_TRX_ID 与 Read View
 中的一些变量及当前事务 ID 进行比较。
 判断是否满足可见性条件:
  1. 如果记录 DB_TRX_ID < m_up_limit_id,那么表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照之前就提交了,所以该记录行的值对当前事务是可见的

  2. 如果 DB_TRX_ID >= m_low_limit_id,那么表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照之后才修改该行,所以该记录行的值对当前事务不可见。跳到步骤 5

  3. m_ids 为空,则表明在当前事务创建快照之前,修改该行的事务就已经提交了,所以该记录行的值对当前事务是可见的

  4. 如果 m_up_limit_id <= DB_TRX_ID < m_low_limit_id,表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照的时候可能处于“活动状态”或者“已提交状态”;所以就要对活跃事务列表 m_ids 进行查找(源码中是用的二分查找,因为是有序的)

    • 如果在活跃事务列表 m_ids 中能找到 DB_TRX_ID,表明:① 在当前事务创建快照前,该记录行的值被事务 ID 为 DB_TRX_ID 的事务修改了,但没有提交;或者 ② 在当前事务创建快照后,该记录行的值被事务 ID 为 DB_TRX_ID 的事务修改了。这些情况下,这个记录行的值对当前事务都是不可见的。跳到步骤 5

    • 在活跃事务列表中找不到,则表明“id 为 trx_id 的事务”在修改“该记录行的值”后,在“当前事务”创建快照前就已经提交了,所以记录行对当前事务可见

  5. 在该记录行的 DB_ROLL_PTR 指针所指向的 undo log 取出快照记录,用快照记录的 DB_TRX_ID 跳到步骤 1 重新开始判断,直到找到满足的快照版本或返回空

2.5 RC 和 RR 隔离级别下 MVCC 的差异

    在事务隔离级别 RC 和 RR (InnoDB 存储引擎的默认事务隔离
级别)下,InnoDB 存储引擎使用 MVCC(非锁定一致性读),
但它们生成 Read View 的时机却不同
  • 在 RC 隔离级别下的 每次select 查询前都生成一个Read View (m_ids 列表)

  • 在 RR 隔离级别下只在事务开始后 第一次select 数据前生成一个Read View(m_ids 列表)