undo log 存储在类型为 FIL_PAGE_UNDO_LOG 页中。 可以从系统表空间中分配空间,也可以从 undo tablespace 中分配空间。 FIL_PAGE_UNDO_LOG 类型页主要分为四部分:

  • File Header,所有页都有的结构
  • Undo Page Header
    • TRX_UNDO_PAGE_TYPE,存储什么类型的 undo log,分类后面会介绍。 不同类型的 undo log 不能存储在同一个页中。 TRX_UNDO_INSERT_REC 类型的放一个页面中,其他类型的放一个页面中。
    • TRX_UNDO_PAGE_START,本页中 undo log 开始的偏移量。
    • TRX_UNDO_PAGE_FREE,本页中,空闲位置开始的偏移量,与最后一条 undo log 的末尾偏移量一样。
    • TRX_UNDO_PAGE_NODE,12 字节,包括4部分内容
      • Pre Node Page Number 和 Pre Node Offset 组成了指向前一个 FIL_PAGE_UNDO_LOG 页的指针;
      • Next Node Page Number 和 Next Node Offset 组成了指向后一个 FIL_PAGE_UNDO_LOG 页的指针;
  • 用于存放 undo log 日志记录的空间
  • File Trailer,所有页都有的结构

01-INSERT 对应的 undo log 记录格式

插入区分乐观插入、悲观插入,

  • 乐观插入,指要插入页的空间充足,足以容纳新插入的记录。
  • 悲观插入,指要插入页的空间不足,需要页分裂。

不管如何,是将一条记录插入。 它对应的回滚操作,就是删除这条插入的记录,TRX_UNDO_INSERT_REC:

  • end of record
  • undo type,TRX_UNDO_INSERT_REC,说明为 INSERT 语句的 undo log。
  • undo no,序列号。
  • table id,可以从 information_schema.INNODB_TABLES 中查询到。
  • 主键各列信息,{}。INSERT 相对应的回滚日志是删除插入的记录,所以这里要记录的信息需要包含如何定位到这条记录。
  • start of record

其中,start of record 和 end of record 是两个指针,指向了 undo log 记录开始、结束的位置。

02-DELETE 对应的 undo log 记录格式

在介绍 记录 和 页 时有提到,数据页中所有的记录通过 next_record 串联起来,形成一个链表。 所有的被标记为删除(即 delete_mask 为1)的记录也会通过 next_record 串联起来,形成垃圾链表。 数据页的 Page Header 中有一个 PAGE_FREE 属性,指向垃圾链表的头部。

首先,我们要先搞明白 DELETE 语句的执行过程。删除分为两个阶段:

  1. 在聚簇索引上根据 id 找到对应的记录,将其 delete_mask 置为1(同时也会修改隐藏列 DB_TRX_ID 和 DB_ROLL_PTR)。此阶段也被称为 delete mark 阶段。
  2. 当执行删除语句的事务提交后,有专门的线程()将 delete_mask 标记为1的记录从正常记录中移除,加入到垃圾链表中(头插)。 同时需要修改一些页面的其他信息,比如页面中的用户记录数量 PAGE_N_RECS、上次插入记录的位置 PAGE_LAST_INSERT、垃圾链表头节点的指针 PAGE_FREE、页面中可重用的字节数量 PAGE_GARBAGE、还有页目录的一些信息等等。 此阶段也被称之为 purge 阶段。

delete mask 阶段对应的 undo log 类型为 TRX_UNDO_DEL_MARK_REC:

  • end of record
  • undo type,TRX_UNDO_DEL_MARK_REC,说明为 delete mask 阶段的 undo log。
  • undo no,序列号。
  • table id
  • info bits
  • old trx_id
  • old roll_pointer,和 old trx_id 一块,记录了修改 delete_mask 前,记录上的值。
  • 主键各列信息,{},主要为了定位记录。
  • index_col_info len,记录了当前、以及下部分内容占用的空间。
  • {},保存了记录中的列(在某个索引中),其在索引中的位置、空间占用、实际值。 这部分内容主要在 purge 阶段使用。
  • start of record

03-UPDATE 对应的 undo log 记录格式

UPDATE 语句的处理方式根据是否更新主键分为两种不同的方案:

  1. 不更新主键。 根据是否改变记录的大小,又可以进一步分为:

    • 就地更新,记录大小不发生变化。 对记录中的每个列来说,其大小都不发生改变。
    • 先删除,再插入。 对记录中的每一列来说,任一列的大小发生了变化。 执行的操作是,先删除、再插入。 删除是彻底删除,并不是 DELETE 中分两阶段的删除。 与 purge 阶段不同的事,并非是某个特定线程去删除,而是由用户线程同步删除。

    针对不更新主键的情况,undo log 类型为 TRX_UNDO_UPD_EXIST_REC(省略了 end of record 之类的共有的信息):

    • old trx_id
    • old roll_pointer
    • 主键各列信息,{},主要为了定位记录。
    • n_updated,记录了共有多个列被更新了。
    • {} 记录了被更新的列的旧值
    • index_col_info len,记录了当前、以及下部分内容占用的空间。
    • {},保存了记录中的列(在某个索引中),其在索引中的位置、空间占用、实际值。
  2. 更新主键。分为两步处理:

    1. 对旧记录进行 delete_mask 阶段的操作。
    2. 插入新的记录。

    所以,这种情况会产生两条 undo log 记录。

04-版本链

在学习 InnoDB 行记录格式时,我们知道在聚簇索引中记录的数据部分 InnoDB 会自动插入三列:DB_ROW_ID\DB_TRX_ID\DB_ROLL_PTR。 其中 DB_ROW_ID 不是必需的,只在没有指定主键且没有唯一列时插入。 其余两列与事务及一致性读视图相关:

  • DB_TRX_ID,每次一个事务对某条聚簇索引记录改动时,都会将事务的 ID 赋给隐藏的 DB_TRX_ID 列
  • DB_ROLL_PTR,每次一个事务对某条聚簇索引记录改动时,旧版本会被写入到 undo log 中,该隐藏列就像一个指针,通过它来找到之前的版本。

对某条记录的每次修改(UPDATEDELETE),都会在 undo log 中记录一个旧版本,旧版本中也包括 DB_ROLL_PTR 列、事务 ID 列。 因此,undo log 中记录的所有版本会形成一个链表,值越旧,它就在链中越靠近末尾的位置,该记录的最新值,记录在聚簇索引中的记录上。

我们借用《MySQL 是怎样运行的:从根儿上理解 MySQL》中的一幅图,加深下对版本链的理解。

图中所示的是插入、并删除一条记录后,形成的版本链。

在读未提交事务隔离级别下,直接读取记录的最新版本即可; 在串行级别下,需要使用锁来保证。 所以,版本链主要应用于读提交和可重复读事务隔离级别。 它要解决的核心问题就是,版本链中的哪些版本是当前事务可见的。