InnoDB undo log

众所周知,InnoDB 存储引擎有一个特点:支持事务。简单来说,事务就是一组要么都做,要么都不做的操作。那么在一个事务执行过程中,遇到异常或手动回滚时,怎么撤销刚刚执行的操作?这就要通过本篇文章要说的 undo log 来实现了。

undo 日志类型

INSERT 操作对应的 undo 日志

INSERT 操作对应的 undo 日志类型为 TRX_UNDO_INSERT_REC,存储 undo type、undo no、table id、主键各列信息<len,value>列表等。其中 len 表示主键列占用的存储空间(例如 INT 类型占用的存储空间长度为 4 个字节),value 表示主键列的实际值。

我们在往一个表里插入一条记录的时候,不仅仅会往聚簇索引中插入记录,二级索引中也是需要插入记录的。我们在回滚操作时,根据这条记录的主键信息在聚簇索引上做删除操作,连带着在二级索引上做删除操作(因为聚簇索引和二级索引记录是一一对应的)。

还记得每条记录都会有三个隐藏列(row_id、trx_id、roll_pointer)么?其中这个 roll_pointer 就是指向 undo log 的指针。当插入一条记录时,生成一个 TRX_UNDO_INSERT_REC 类型的 undo log,同时插入的这条记录的 roll_pointer 指向该 undo log。

DELETE 操作对应的 undo 日志

之前介绍数据页结构的时候说过,数据页中的记录会根据记录头信息中的 next_record 属性组成一个单向链表。当删除记录的时候,这些记录也会根据记录头信息中的 next_record 属性组成一个单向链表,我们称之为垃圾链表。Page Header 部分有一个称之为 PAGE_FREE 的属性,它指向该垃圾链表中的头节点。

当我们用 delete 删除一条记录的时候,其实分为两个阶段:

  1. delete mark 阶段

    仅仅将记录的 delete_mark 标识位设置为 1,不会将该记录移动到垃圾链表。在删除语句所在的事务提交之前,被删除的记录一直都处于这种状态。

  2. purge 阶段

    当该删除语句所在的事务提交之后,会有专门的线程( purge thread )来真正的把记录删除掉。真正的删除也就是把记录移动到垃圾链表(头插法,所以会涉及到 PAGE_FREE 的修改),并且修改诸如页面用户记录数量、上次插入记录位置等信息。

由于阶段二是在事务提交后执行的,所以不考虑了。这里只考虑对阶段一所做操作进行回滚。InnoDB 设计了一种 TRX_UNDO_DEL_MARK_REC 类型的 undo 日志。它存储 undo type、undo no、table id、old trx_id、old roll_pointer、主键各列信息<len,value>列表、索引列各列信息<pos, len, value>列表等信息。

TRX_UNDO_INSERT_REC 相比,多了几个属性:

  • old trx_id:旧事务id
  • old roll_pointer:旧回滚指针
  • 索引列各列信息:这部分信息主要是用在事务提交后 purge 阶段中使用的

想象一下:当我们插入一条记录后,该记录的 roll_pointer 指向 TRX_UNDO_INSERT_REC 类型的 undo log(这里我们称之为 insert undo log),这时我们用 delete 删除该记录(事务未提交,执行阶段一),生成一条 TRX_UNDO_DEL_MARK_REC 类型的 undo log(这里我们称之为 delete undo log)。此时该记录仍存在,且 roll_pointer 指向 delete undo log,delete undo log 的 old roll_pointer 指向 insert undo log

UPDATE 操作对应的 undo 日志

在使用 update 更新数据时,可以分为不更新主键和更新主键两种情况

不更新主键的情况

在不更新主键的情况,又可以分为被更新的列占用的存储空间不发生变化和发生变化两种情况

  • 不发生变化

    对于被更新的每个列来说,如果更新前后占用存储空间不变,那么就可以直接在原记录上直接修改。

  • 发生变化

    如果有任意一个被更新的列,更新前后占用存储空间发生了变化,那么就不能直接在原记录上直接修改。而是需要先在聚簇索引中将该记录删除,再根据更新后的值创建一条新的记录插入。

    注意一下,这里所说的删除不是上面说的 delete mark,而是由用户线程同步执行的真正删除。

针对不更新主键的情况,InnoDB 设计了一种类型为 TRX_UNDO_UPD_EXIST_REC 的 undo 日志,它存储 undo type、undo no、table id、old trx_id、old roll_pointer、主键各列信息<len,value>列表、n_updated、被更新列更新前信息<pos, old_len, old_value>列表、索引列各列信息<pos, len, value>列表等信息。

TRX_UNDO_DEL_MARK_REC 相比,多了几个属性:

  • n_updated:有多少个列被更新
  • 被更新列更新前信息
  • 如果更新的列包含索引列,那么也会添加 索引列各列信息 这个部分,否则的话是不会添加这个部分的。

更新主键的情况

在 update 更新主键这种情况下,InnoDB 分两步执行:

  1. 将旧记录进行 delete mark 操作

    就是说在 update 语句所在事务提交前,不会把旧记录移动到垃圾链表。因为此时别的事务可能还需要访问到该记录,如果这里真正删了旧记录,别的事务就访问不到了。也就是 MVCC 相关的内容了,后面会说到。

  2. 根据更新后各列的值创建一条新记录,重新定位并将其插入到聚簇索引中

针对 update 语句更新记录主键值的这种情况:

  • 在对该记录进行 delete mark 操作前,会记录一条类型为 TRX_UNDO_DEL_MARK_REC 的 undo日志;

  • 之后插入新记录时,会记录一条类型为 TRX_UNDO_INSERT_REC 的 undo日志,也就是说每对一条记录的主键值做改动时,会记录 2 条 undo日志。

FIL_PAGE_UNDO_LOG 页面

正如 FIL_PAGE_INDEX 类型的页是用来存储索引的, 这里要说的 FIL_PAGE_UNDO_LOG 类型的页面(后文称 undo 页面)是用来专门存储 undo 日志的,除了 File Header 和 File Trailer 这种页面通用的部分,Undo Page Header 是 undo 页面所特有的,含有以下属性:

  • TRX_UNDO_PAGE_TYPE:该页面是用来存储什么类型的 undo 日志

    可选值有两个:TRX_UNDO_INSERT(TRX_UNDO_INSERT_REC 类型属于该类) 和 TRX_UNDO_UPDATE(除 TRX_UNDO_INSERT_REC 类型以外的都属于该类);之所以把 undo 日志分成两个大类,是因为类型为 TRX_UNDO_INSERT_REC 的 undo 日志在事务提交后可以直接删除掉,而其他类型的 undo 日志还需要为所谓的 MVCC 服务,不能直接删除掉,对它们的处理需要区别对待。

  • TRX_UNDO_PAGE_START:表示在当前页面中是从什么位置开始存储 undo 日志的

    或者说表示第一条 undo 日志在本页面中的起始偏移量。

  • TRX_UNDO_PAGE_FREE:表示当前页面中存储的最后一条 undo 日志结束时的偏移量

    或者说从这个位置开始,可以继续写入新的 undo 日志。

  • TRX_UNDO_PAGE_NODE:一个链表节点包含一下几个部分:

    • List Length 表明该链表一共有多少节点。
    • First Node Page Number 和 First Node Offset 的组合就是指向链表头节点的指针。
    • Last Node Page Number 和 Last Node Offset 的组合就是指向链表尾节点的指针。

undo 页面链表

单个事务的 undo 页面链表

在一个事务执行过程中可能产生很多 undo 日志,这些日志可能一个页面放不下,需要放到多个页面中,这些页面就通过我们上边介绍的 TRX_UNDO_PAGE_NODE 属性连成了链表;为事务分配链表策略如下:

  • 刚刚开启事务时,一个 undo 页面链表也不分配
  • 事务执行过程中向普通表中插入记录或者执行更新记录主键的操作之后,分配一个普通表的 insert undo 链表
  • 事务执行过程中删除或者更新了普通表中的记录之后,分配一个普通表的 update undo 链表
  • 事务执行过程中向临时表中插入记录或者执行更新记录主键的操作之后,分配一个临时表的 insert undo 链表
  • 事务执行过程中删除或者更新了临时表中的记录之后,就会为其分配一个临时表的 update undo 链表。

多个事务的 undo 页面链表

为了尽可能提高 undo 日志的写入效率,不同事务执行过程中产生的 undo 日志需要被写入到不同的 undo 页面链表中。

undo 日志写入过程

Segment(段)

段是一个逻辑概念,简单来说就是一些零散的页面和一些完整的区(每64个页作为一个区)组成的。

每一个段对应一个 INODE Entry 结构,这个 INODE Entry 结构描述了这个段的各种信息,比如段的 ID,段内的各种链表基节点,零散页面的页号有哪些等信息。为了定位到一个 INODE Entry,InnoDB 设计了一个 Segment Header (10个字节)的结构:

  • Space ID of the INODE Entry:INODE Entry 结构所在的表空间ID
  • Page Number of the INODE Entry:INODE Entry 结构所在的页面页号
  • Byte Offset of the INODE Entry:INODE Entry 结构在该页面中的偏移量

有了 Segment Header 的表空间ID、页面号、偏移量等信息,就可以很容易定位到对应的 INODE Entry 了。

Undo Log Segment Header

InnoDB 规定每一个 undo 页面链表都对应着一个段,称之为 Undo Log Segment。也就是说链表中的页面都是从这个段里边申请的,所以他们在 undo 页面链表的第一个页面(first undo page)中设计了一个称之为 Undo Log Segment Header 的部分,这个部分中包含了该链表对应的段的 segment header 信息以及其他的一些关于这个段的信息。

也就是说 undo 页面链表第一个页面比其他普通 undo 页面多个 Undo Log Segment Header 部分,结构如下:

  • TRX_UNDO_STATE:本 undo 页面链表处在什么状态。一个 Undo Log Segment 可能处在的状态包括:

    • TRX_UNDO_ACTIVE:活跃状态,也就是一个活跃的事务正在往这个段里边写入 undo 日志。

    • TRX_UNDO_CACHED:被缓存的状态。处在该状态的 undo 页面链表等待着之后被其他事务重用。

    • TRX_UNDO_TO_FREE:对于 insert undo 链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态。

    • TRX_UNDO_TO_PURGE:对于 update undo 链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态。

    • TRX_UNDO_PREPARED:包含处于 PREPARE 阶段的事务产生的 undo 日志。

  • TRX_UNDO_LAST_LOG:本 undo 页面链表中最后一个 Undo Log Header 的位置。

  • TRX_UNDO_FSEG_HEADER:本 undo 页面链表对应的段的 Segment Header 信息(通过这个信息可以找到该段对应的 INODE Entry)。

  • TRX_UNDO_PAGE_LIST:Undo页面链表的基节点。

前面说过,undo 页面的 Undo Page Header 部分有个属性 TRX_UNDO_PAGE_NODE,undo 页面通过这个属性连接成一个链表。这个 TRX_UNDO_PAGE_LIST 属性代表着这个链表的基节点,当然这个基节点只存在于 undo 页面链表的第一个页面中。

###Undo Log Header

我们把同一个事务向一个 undo 页面链表中写入的 undo日志算是一个组,例如事务 A 需要分配两个 undo 页面链表,就会写入两个组的 undo 日志。在每写入一组 undo 日志前,会先记录关于这个组的一些信息。这些信息存储在 Undo Log Header 中。

也就是说,undo 页面链表的第一个页面在真正写入 undo 日志前,其实都会被填充 Undo Page Header、Undo Log Segment Header、Undo Log Header 这3个部分,Undo Log Header 结构如下:

  • TRX_UNDO_TRX_ID:生成本组 undo 日志的事务 id。

  • TRX_UNDO_TRX_NO:事务提交后生成的一个需要序号,使用此序号来标记事务的提交顺序(先提交的此序号小,后提交的此序号大)。

  • TRX_UNDO_DEL_MARKS:标记本组 undo 日志中是否包含由于 delete mark 操作产生的 undo 日志。

  • TRX_UNDO_LOG_START:表示本组 undo 日志中第一条 undo 日志的在页面中的偏移量。

  • TRX_UNDO_XID_EXISTS:本组 undo 日志是否包含 XID 信息。

  • TRX_UNDO_DICT_TRANS:标记本组 undo 日志是不是由 DDL 语句产生的。

  • TRX_UNDO_TABLE_ID:如果 TRX_UNDO_DICT_TRANS 为真,那么本属性表示 DDL 语句操作的表的 table id。

  • TRX_UNDO_NEXT_LOG:下一组的 undo 日志在页面中开始的偏移量。

  • TRX_UNDO_PREV_LOG:上一组的 undo 日志在页面中开始的偏移量。

  • TRX_UNDO_HISTORY_NODE:一个12字节的 List Node 结构,代表一个称之为 History 链表的节点。

一般来说,一个 undo 页面链表只存储一个事务执行过程中产生的一组 undo 日志,但是在某些情况下,可能会在一个事务提交之后,之后开启的事务重复利用这个 undo 页面链表,这样就会导致一个 undo 页面中可能存放多组 undo 日志,TRX_UNDO_NEXT_LOG 和 TRX_UNDO_PREV_LOG 就是用来标记下一组和上一组undo 日志在页面中的偏移量的。

小结

对于没有重用的 undo 页面链表来说,链表的第一个页面在写入 undo 日志前,会填充 Undo Page Header、Undo Log Segment Header、Undo Log Header 这3个部分;后面的其他 undo 页面在写入 undo 日志前,只会填充 Undo Page Header。

链表的 List Base Node 存放到 undo 页面链表的第一个页面的 Undo Log Segment Header 部分,List Node 信息存放到每一个 undo 页面的 Undo Page Header 部分。

重用 undo 页面

前面说到为了提高多个事务并发写入 undo 日志的性能,InnoDB 会为每个事务分配单独的 undo 页面链表(最多四个)。但是大部分情况下,一个事务可能只修改了一条或几条记录,也就是产生了很少的 undo 日志,如果为这些事务都创建单独的 undo 页面链表(可能只有一个 undo 页面)来存储很少的 undo 日志的话,有点浪费了好像。所以 InnoDB 规定在事务提交后,某些情况下该事务的 undo 页面可以被其它事务重用。能否被重用的条件如下:

  • 该链表中只有一个 undo 页面
  • 该 undo 页面的使用量小于 3/4

前面说到,undo 页面链表分为 insert undo 链表和 update undo 链表,它们在重用时的规则也是不一样的:

  • insert undo 链表

    insert undo 链表中只存储类型为 TRX_UNDO_INSERT_REC 的 undo 日志,这种类型的 undo 日志在事务提交后就没有用了,如果符合上述两个条件的话,新事务重用该 undo 页面时可以直接覆盖写入。

  • update undo 链表

    对于 update undo 链表中的 undo 日志,在事务提交后可能还会有用(MVCC相关),所以不能像上面说的那样直接覆盖写入,这样就发生了一个 undo 页面中写入了多组 undo 日志的情况。

Rollback Segment(回滚段)

一个事务最多会分配 4 个 undo 页面链表,所以同一时刻可能会有很多的 undo 页面链表。InnoDB 为了更好的管理这些链表,用 Rollback Segment Header 页面来进行管理:存储每个 undo 页面链表的第一个页面的页号

每一个 Rollback Segment Header 页面都对应着一个段,这个段就称为 Rollback Segment(回滚段)。与我们之前介绍的各种段不同的是,这个 Rollback Segment 里其实只有一个页面。Rollback Segment Header 页面存储内容如下:

  • TRX_RSEG_MAX_SIZE:管理的所有 undo 页面链表中的 undo 页面数量之和的最大值。

    换句话说,本 Rollback Segment 中所有 undo 页面链表中的 undo 页面数量之和不能超过 TRX_RSEG_MAX_SIZE 代表的值。

  • TRX_RSEG_HISTORY_SIZE:History 链表占用的页面数量。

  • TRX_RSEG_HISTORY:History 链表的基节点。

  • TRX_RSEG_FSEG_HEADER:本 Rollback Segment 对应的10字节大小的 Segment Header 结构,通过它可以找到本段对应的 INODE Entry。

  • TRX_RSEG_UNDO_SLOTS:各个 undo 页面链表的第一个页面的页号集合,也就是 undo slot 集合。

    一个页号占用 4 个字节,对于 16KB 大小的页面来说,这个 TRX_RSEG_UNDO_SLOTS 部分共存储了 1024 个 undo slot,所以共需1024 × 4 = 4096个字节。

申请 undo 页面链表

对于一个 Rollback Segment Header 页面来说,初始情况下,各个 undo slot 都是 FIL_NULL,表示不指向任何页面。当需要为事务分配 undo 页面链表的时候,从第一个 undo slot 开始,判断是不是 FIL_NULL:

  • 如果是 FIL_NULL,那么在表空间中新创建一个段(也就是 Undo Log Segment),然后从段里申请一个页面作为 undo 页面链表的第一个页面,然后把该 undo slot 的值设置为刚刚申请的这个页面的页号,这样也就意味着这个 undo slot 被分配给了这个事务。
  • 如果不是 FIL_NULL,说明该 undo slot 已经指向了一个 undo 页面链表,那就跳到下一个 undo slot,判断该 undo slot 的值是不是 FIL_NULL,重复上边的步骤。

如果这 1024 个 undo slot 的值都不为 FIL_NULL,那么当前需要分配链表的事务就会申请不到,报错如下:

1
Too many active concurrent transactions

当一个事务提交后,对应的 undo slot 怎么处理呢?规则如下:

  • 如果该 undo slot 对应的 undo 页面链表符合被重用的条件

    该 undo slot 就处于被缓存的状态,undo 页面的 Undo Log Segment Header 部分的 TRX_UNDO_STATE 属性被设置为 TRX_UNDO_CACHED;被缓存的 undo slot 会被加入到一个链表: insert undo cached 链表/update undo cached 链表;

    一个回滚段就对应着上述两个 cached 链表,如果有新事务要分配 undo slot 时,先从对应的 cached 链表中找。如果没有被缓存的 undo slot,才会到回滚段的 Rollback Segment Header 页面中再去找。

  • 如果该 undo slot 对应的 undo 页面链表不符合被重用的条件

    • 如果对应的 undo 页面链表是 insert undo 链表

      则该 undo 页面链表的 TRX_UNDO_STATE 属性会被设置为 TRX_UNDO_TO_FREE;之后该 undo 页面链表对应的段会被释放掉;然后把该 undo slot 的值设置为 FIL_NULL。

    • 如果对应的 undo 页面链表是 update undo 链表

      则该 undo 页面链表的 TRX_UNDO_STATE 属性会被设置为 TRX_UNDO_TO_PRUGE;将该 undo slot 的值设置为 FIL_NULL;然后将本次事务写入的一组 undo 日志放到所谓的 History链表中(需要注意的是,这里并不会将 undo 页面链表对应的段给释放掉,因为这些 undo 日志还有用呢~)。

多个回滚段

假设一个读写事务执行过程中只分配 1 个 undo 页面链表,那 1024 个 undo slot 也只能支持 1024 个读写事务同时执行,再多了就崩溃了。所以 InnoDB 定义了 128 个回滚段。

每个回滚段都对应着一个 Rollback Segment Header 页面,有 128个 回滚段,自然就要有 128 个 Rollback Segment Header 页面,那么存哪呢?在系统表空间的第 5 号页面的某个区域包含了 128 个 8 字节大小的格子,
每个格子存着 Space ID 和 Page number,对应着一个 Rollback Segment Header 页面。

  • 第 0 号、第 33~127 号回滚段属于一类。其中第 0 号回滚段必须在系统表空间中,第 33~127 号回滚段既可以在系统表空间中,也可以在自己配置的 undo 表空间中

  • 第 1~32 号回滚段属于一类。这些回滚段必须在临时表空间(对应着数据目录中的ibtmp1文件)中。

针对普通表和临时表划分不同种类的回滚段的原因:在修改针对普通表的回滚段中的 undo 页面时,需要记录对应的 redo 日志,而修改针对临时表的回滚段中的 undo 页面时,不需要记录对应的 redo 日志。

分配 undo 页面链表过程

  1. 事务在执行过程中对普通表的记录首次做改动之前,首先会到系统表空间的第 5 号页面中分配一个回滚段(其实就是获取一个 Rollback Segment Header 页面的地址)。

  2. 在分配到回滚段后,首先看一下这个回滚段的两个 cached 链表有没有已经缓存了的 undo slot,比如如果事务做的是 INSERT 操作,就去回滚段对应的 insert undo cached 链表中看看有没有缓存的 undo slot;
    如果事务做的是 DELETE 操作,就去回滚段对应的 update undo cached 链表中看看有没有缓存的 undo slot。如果有缓存的 undo slot,那么就把这个缓存的 undo slot 分配给该事务。

  3. 如果没有缓存的 undo slot 可供分配,那么就要到 Rollback Segment Header 页面中找一个可用的 undo slot 分配给当前事务。

  4. 找到可用的 undo slot 后,如果该 undo slot 是从 cached 链表中获取的,那么它对应的 Undo Log Segment 已经分配了,否则的话需要重新分配一个 Undo Log Segment,然后从该 Undo Log Segment 中申请一个页面作为 undo 页面链表的 first undo page。

  5. 然后事务就可以把 undo 日志写入到上边申请的 undo 页面链表了。

回滚段相关配置

  • innodb_rollback_segments:配置回滚段的数量,范围1-128

    固定有 32 个针对临时表的可用回滚段。值大于 33 时,针对普通表的回滚段数量为 innodb_rollback_segments - 32

  • innodb_undo_directory:指定 undo 表空间所在的目录,默认在数据目录

  • innodb_undo_tablespaces:指定 undo 表空间的数量

    第 33~127 号回滚段可以平均分布到不同的 undo 表空间中

MVCC 简介

事务并发可能遇到的问题

  1. 脏读:事务读取到了另一个还未提交的事务修改的数据
  2. 不可重复读:事务连续读取同一条记录数据不一致,因为期间数据被另一个事务修改了
  3. 幻读:事务连续两次读取数据,第二次读取比第一次读取多了些数据,因为期间另一个事务插入了数据

事务隔离级别

  1. READ UNCOMMITTED:可能发生脏读、不可重复读和幻读
  2. READ COMMITTED:可能发生不可重复读和幻读
  3. REPEATABLE READ:可能发生幻读(InnoDB 使用 mvcc 和 next-key lock 解决该问题
  4. SERIALIZABLE:通过加锁方式读取记录,不会发生上述问题

ReadView

前面说到记录的回滚指针 roll_pointer 指向 undo log,会形成一个版本链,事务在读取记录时在不同的隔离级别下可能会看到不同的记录版本。这个功能通过 ReadView 来实现。

ReadView 主要包含以下几个部分:
m_ids:生成 ReadView 时系统中活跃的事务 id 集合
min_trx_id:生成 ReadView 时系统中活跃的最小事务 id,也就是 m_ids 中的最小值
max_trx_id:生成 ReadView 时系统中应该分配给下一个事务的 id 值,比 m_ids 的最大值要大
creator_trx_id:生成该 ReadView 的事务 id

由于 READ UNCOMMITTED 级别下每次读取最新记录,SERIALIZABLE 级别下通过加锁访问数据,
所以 ReadView 仅对 READ COMMITTED 和 REPEATABLE READ 有效。

怎么通过这个 ReadView 来判别当前事务能看到的版本呢?过程如下:

  1. 如果被访问版本的 trx_id 与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
  2. 如果被访问版本的 trx_id 小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。
  3. 如果被访问版本的 trx_id 大于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。
  4. 如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间,那就需要判断一下 trx_id 是不是在 m_ids 列表中,
    如果在,说明生成 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;
    如果不在,说明生成 ReadView 时生成该版本的事务已经被提交,该版本可以被访问;
  5. 如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。
    如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。

ReadView 生成时机:

  • READ COMMITTED —— 每次读取数据前都生成一个ReadView(不能保证可重复读)
  • REPEATABLE READ —— 在第一次读取数据时生成一个ReadView(能够保证可重复读)

RR 级别下幻读问题

上面说到 RR 级别下,会在第一次读取数据时就生成一个 ReadView,所以可以保证可重复读。但是 MVCC 能否解决幻读问题呢?答案是只能解决一部分情况下的幻读问题,也就是说能解决快照读情况下的幻读问题,但是不能解决当前读情况下的幻读问题(使用 next-key lock 来解决)。

  • 快照读

    快照读就是上面说到的 ReadView,可以读到数据的历史版本。普通的 select 都属于快照读。

  • 当前读

    当前读就是读取记录的最新版本。insert/update/delete/select…in share mode/select…for update 都属于当前读。

有关加锁的内容不在本文讨论范围。

总结

  • InnoDB 通过 undo log 来实现事务操作撤销的功能
  • undo log 在 FIL_PAGE_UNDO_LOG 类型的页面中
  • 每个事务都有最多四个 undo 页面链表,其中的 undo 页面能否重用视情况而定
  • InnoDB 使用 Rollback Segment Header 类型页面来管理多个 undo 页面链表
  • InnoDB 的 MVCC 可以通过 ReadView 解决可重复读和部分幻读问题

参考书籍: