undo log详解

undo log详解

在介绍事务实现原理时候,我们简单了解过undo log,它可以帮助我们实现事务的原子性。本文把undo log拎出来,详细介绍一下。

在事务的执行过程中,可能遇到各种错误,此时为了保证事务的原子性,我们需要回滚当前事务已经执行的修改。那么怎么回滚这些修改呢?一种直接的方法就是,每次执行insert、delete、update语句时,我们都要留一手,用某种机制记录下数据修改前的样子,这样当我们需要回滚时,我们就能将修改的数据恢复原样。这就是undo log需要实现的功能。

1 Undo log格式

1.1 insert对应的undo log

对于insert操作,对应的undo log很简单,我们只需要记录插入数据的主键信息即可。当需要回滚时,我们只需要根据其主键信息把它删除即可。其格式如下图所示:

看到这里,我们可能有一个疑惑:undo日志是如何和数据记录对应上的?也就是说,我们要回滚时,如何找到目标记录对应的undo log?

这里就要提到数据记录中的隐藏字段:roll_pointer了。我们知道对于任意一张表,MySQL会自动创建两个隐藏字段roll_point和trx_id,分别表示回滚指针和事务id。其中roll_pointer就是一个指向undo log的指针。

假如,我们在某个事务中向表中插入两条数据,此时会生成如下图所示的undo log:

1.2 delete对应的undo log

在“InnoDB数据的存储”一文中,我们知道每一行数据对应一个record,它们以链表形式存储在page中,这个链表称之为“正常链表”。Page Header中还有一个PAGE_FREE属性,对于被删除的记录,其实也是以链表形式连接在一起的,称之为“垃圾链表”。

假设某一时刻,页面中记录分布情况如下图所示:

假设现在准备使用DELETE语句删除正常记录链表中最后一条记录,这个过程需要经历两个阶段:

  • 阶段1:delete_mark阶段,将记录的delete_flag标识位设置成1,其他的不做修改

  • 阶段2:purge阶段,当删除语句所在事务提交后,会有专门线程来真正把记录删除,即移动到垃圾链表中。

    为什么不在事务提交后,立即移动到垃圾链表?因为MVCC中可能会用到,详情看“InnoDB中的MVCC”一文。

在事务提交前,只会经历阶段1,一旦事务提交了,我们就不需要回滚该操作了,因此设计undo日志格式时只需要考虑阶段1即可。InnoDB中设计了一种类型为TRX_UNDO_DEL_MARK_REC的undo log,来处理这些场景。其格式如下图所示:

其中有2个特别的字段:trx_id和roll_pointer,用于存储旧记录的trx_id和roll_pointer。这样作的一个好处,就是可以通过undo日志的roll_pointer找到上一次对该记录修改时产生的undo log,也就是形成了我们常说的版本链(实现MVCC的基础)。

1.3 update对应的undo log

对于update操作,更新主键和不更新主键两种情况有着截然不同的处理方案。

1.3.1 不更新主键

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

  • 就地更新

    只有当所有要更新的列占用空间都不变时,才能使用就地更新。

    所谓就地更新,就是在旧记录上直接修改对应列的值。

  • 删除后插入

    如果有任何一列更新前后占用空间大小不一致,就需要使用该种更新方式,即先删除旧记录,再插入一条新记录。

    注意,这里的删除和前文提到的delete_mark不同,是真正删除,即把该记录移到垃圾链表中。

对于不更新主键的情况,InnoDB设计了一种类型为TRX_UNDO_EXIST_REC的undo日志,其结构如下图所示:

1.3.2 更新主键

由于记录在页面中是按照主键升序排列的,如果我么更新了主键,那么更新前后该记录可能相隔很远,甚至隔了好几个页面。

对于这种情况,InnoDB在聚簇索引中分了两步进行处理:

  • 步骤1:将旧记录进行delete_mark操作,和前文介绍delete undo log一致
  • 步骤2:根据更新后各列值,插入一条新的记录

2 Undo log的存储

2.1 FIL_PAGE_UNDO_LOG页面

undo log和数据库中的表一样,它是存储在系统表空间或者单独的undo log表空间中,用一种类型为FIL_PAGE_UNDO_LOG的页来专门存储undo log信息。其通用结构如下图所示:

其中Undo Page Header是undo 页面特有的,其结构如下图所示:

  • TRX_UNDO_PAGE_TYPE:准备存储什么类型的undo log
  • TRX_UNDO_PAGE_START:当前页面从什么位置开始存储undo日志,即第一条undo log的偏移量
  • TRX_UNDO_PAGE_FREE:与上面一条相对应,表示最后一条undo日志结束位置的偏移量
  • TRX_UNDO_PAGE_NODE:页面链表结构,指向下一个undo 页面,下面马上用到。

2.2 Undo页面链表

一个事务中可能生成多条undo log,这些undo log在一个页面中可能放不下,因此通过前文提到的TRX_UNDO_PAGE_NODE属性将这些页面连成链表,如下图所示:

在一个事务中可能会混合执行insert、update、delete语句,生成不同类型的undo log,但是一个页面只能存储一种类型undo log,因此系统中会有多条undo 页面链表,如下图所示:

另外,在InnoDB中,对普通表和临时表进行改动生成的undo log要分别记录,因此一个事务最多有4个以Undo页面为节点组成的链表,如下图所示:

读到这里,我们发现前面提到的都是“一个事务”,难道每个事务都有自己单独的undo 链表?

是这样的,为了提高undo日志的写入效率,不同事务有自己单独的undo链表,如下图所示:

3 Undo log的写入

在InnoDB中,每个Undo页面链表都对应着一个段,称为Undo Log Segment,链表中每个页面都是从这个段中申请的。所以在Undo页面列表的第一个页面中设计了一个名为Undo Log Segment Header的属性,其中包含了该链表对应的段信息。如下图所示:

可以看到和普通undo 页相比,链表第一个页面多了一个Undo Log Segment Header,其具体结构如下:

  • TRX_UNDO_STATE:表示本Undo页面列表的状态,可能为
    • 活跃状态:有一个活跃事务,正在向该undo链表中写日志
    • 被缓存状态:处于该状态的Undo 页面链表等待之后被其他事务重用
    • 等待被释放状态:对于insert 链表,如果事务提交后,该链表不能被重用,就处于该状态
    • 等待被purge状态:对于update链表,如果事务提交后,该链表不能被重用,就处于该状态
    • PREPARED状态,处于此状态的Undo页面链表用于存储处于prepare阶段的事务产生的日志(分布式事务中的概念)
  • TRX_UNDO_LAST_LOG:最后一个Undo Log Header的位置
  • TRX_UNDO_FSEG_HEADER:本链表对应的段的Segment Header信息
  • TRX_UNDO_PAGE_LIST:Undo页面链表的及节点。

InnoDB中undo log的写入方式十分简单,就是一条紧挨着一条,写入page中。不过undo链表第一个页面有所不同,还多了一个Undo Log Header属性,记录了undo log分组相关信息。因此一个真实undo链表可能是这样的:

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2022 Yin Peng
  • 引擎: Hexo   |  主题:修改自 Ayer
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信