redo log详解

redo log详解

之前学习事务的时候,介绍过redo log,其核心功能就是服务器宕机后进行数据恢复。本文把它单独拎出来,详细介绍一下。

在了解Redo log更具体内容之前,我们先回答一个问题:为什么要有redo log?

学习过BufferPool,我们知道InnoDB读取/更新数据操作都是在BufferPool中进行的,在某个时间点统一将BufferPool中的脏页同步到磁盘中。但是如果系统发生故障,宕机了,BufferPool中还有脏页没来得及同步到磁盘中,这时就会发生数据丢失。

为了避免这种情况,我们就需要一种机制,保存BufferPool中数据更新的操作,在系统恢复时,帮助我们恢复宕机前没来得及同步的脏页数据。这就是redo log要完成的功能。

也就是说,对于更新操作,我们不仅要更新BufferPool中的数据,我们还要将该更新操作记录到redo log中。为了节约空间,redo log中记录的是物理页的变化。比如,某个事务将系统表空间第100号页面中偏移量为1000处的字节从1更改为2,我们只需要进行如下记录:

“将第0号表空间第100号页面中偏移量为1000处的值改为2”。

1 Redo log格式

InnoDB中针对不同场景,设计了多种类型的redo日志,大部分类型的结构如下图所示:

  • type:日志类型,MySQL5.7.22中,InnoDB一共设计了53中日志类型
  • space ID:表空间ID
  • page number:页号
  • data:这条日志的具体内容

2 怎么存?

2.1 Min-Transaction

InnoDB中redo log并不是以每一条log作为最小恢复单位,而是将redo log划分成一个个不可分割的组,以组为最小单位进行恢复。在数据恢复时,对于一组日志,要么把整组日志都恢复(组是完整的),要么整组日志都不恢复(组是不完整的)。

那怎么知道一组日志是不是完整的呢?InnoDB中在每组redo log末尾添加一个特殊类型的日志MLOG_MULTI_REC_END,如下图所示:

这样在系统崩溃后重新启动进行数据恢复时,只有解析到类型为MLOG_MULTI_REC_END的redo log,才认为是一组完整的日志。

看到这里我们有两个问题:

  1. 为什么要分组?

    一条SQL更新语句可能会生成很多条redo 日志,比如一个插入操作,可能要同时更新聚簇索引、二级索引、max row id等各种不同页数据,如果出现页分裂,生成的日志数量就更多了。下图展示了需要页分裂的插入过程:

    InnoDB的设计者认为,向某个B+树中插入一条记录的过程必须是原子的,不能说插入一半之后就停止了。比如在悲观插入(带有页分裂的插入)过程中,新的页面分配好了,数据也复制好了,新的记录也已经插入了,但是没有向内节点插入一条目录项。那么这个过程也就是不完整的,就会形成一棵不正确的B+树。

因此,我们需要将一条语句的具体执行过程进行分组,每个分组包含若干个不可分割的过程,每个分组称之为min-transaction。

  1. 怎么分组?

    前面我们提到,分组是按照min-transaction进行的,那么什么是min-transaction呢?InnoDB的设计者把对底层页面进行一次原子访问的过程称之为一个min-transaction(MTR)。这样一个事务可以包含若干条语句,每一条语句又包含若干个MRT,如下图所示:

2.2 写入过程

2.2.1 log buffer

存储格式

和Buffer Pool一样,InnoDB为了解决磁盘速度过慢的问题引入了redo log buffer,写入redo log时,先写入buffer,然后再统一刷新到磁盘中。

为了更好地管理redo log,InnoDB把生成的redo log存放在512字节的页中(和数据页、索引页差不多),这里称之为block,其格式如下:

  • LOG_BLOCK_HDR_NO:每一个block 都有一个大于0 的唯一编号,该属性就表示该编号值。
  • LOG_BLOCK_HDR_DATA_LEN:表示block 中已经使用了多少字节, 初始值为12 (因为log block body从第12 个字节处开始)。随着往block中写入的日志越来越多,该属性值也跟着增长。如果log block body已经被全部写满,那么该属性的值被设置为512。
  • LOG_BLOCK_FIRST_REC_GROUP: 一个MTR 会生成多条redo 日志记录,这个MTR 生成的这些redo日志记录被称为一个redo日志记录组( redo log record group)。LOG_BLOCK_FIRST_REC_GROUP就代表该block中第一个MTR 生成的redo日志记录组的偏移量, 其实也就是这个block中第一个MTR 生成的第一条redo日志记录的偏移量(如果一个MTR 生成的redo日志横跨了好多个block,那么最后一个block 中的LOG_BLOCK_FIRST_REC_GROUP属性就表示这个MTR 对应的redo日志结束的地方,也就是下一个MTR 生成的时0 日志开始的地方) 。
  • LOG_BLOCK_CHECKPOINT_NO:表示checkpoint 的序号,checkpoint是后续内容的重点, 现在先不用消楚它的意思,少安毋躁。
  • LOG_BLOCK_CHECKSUM:表示该block的校验值,用于正确性校验。

log buffer就是内存中若干个连续的redo log block,在系统启动时候就申请好了,默认大小为16MB。

写入log buffer

向log buffer中写入redo 日志的过程是顺序写入的,当该block的空闲空间用完了,再写入下一个block。InnoDB中提供了一个全局变量buf_free,致命后续写入的redo 日志应该写到log buffer的哪个位置,如下图所示:

2.2.2 log file

刷盘时机

在哪些情况下会将log buffer中的数据刷新到磁盘中呢?

  • log buffer空间不足时:当前写入的log占满总空间50%时,会进行刷盘操作
  • 事务提交:为了保证事务的持久性,事务提价后,必须将buffer中数据刷新到磁盘,否则发生系统崩溃,会丢失修改
  • 刷新脏页前:将某个脏页刷新到磁盘前,会保证先将该脏页对应的redo日志刷新到磁盘中
  • 后台线程:后台有一个线程,以大概1s一次的频率进行buffer刷盘操作
  • 正常关闭服务器时
  • 做checkpoint时
文件格式

磁盘上的redo日志文件不止一个,而是以一个日志文件组的形式出现的。文件组中每个文件大小、格式都是一样的,由下面两个部分组成:

  • 前2048字节:4个block,存储管理信息
  • 其余字节:存储log buffer中的block镜像

这里重点前4个block中的信息,其格式如下图所示:

  • log file header:描述文件日志的整体属性

  • checkpoint1:记录check point相关属性

  • checkpoint2:和checkpoint1格式一样

2.3 log sequence number(LSN)

自系统运行开始,就在不断修改页面,也就意味着会不断生成redo日志,redo日志数量是在不断增加的。InnoDB中设计了一个lsn变量,用来表示写入的日志总量。

lsn的初始值是8704:

  • 当某个mtr产生的一组redo日志较小,还未占满整个block时,写入多少字节日志,lsn的值就增加多少。

  • 当某个mtr产生的一组redo日志很大,一个block放不下时,lsn得到还要额外增加block header和block trailer所占字节数,如下图所示:

2.3.1 lsn和文件偏移量对应关系

lsn的初始值为8704,redo日志文件组中偏移量起始值为2048。因此两者对应关系如下图所示:

2.3.2 flushed_to_disk_lsn

redo日志是先写到log buffer中,之后才会被刷新到磁盘的redo日志文件中,因此还需要一个变量表示buffer中哪些log已经被刷新到磁盘了,这个变量就是buf_next_to_write。

2.3.3 flush链表中的lsn

buffer pool中的脏页是记录在flush链表中,第一次修改某个页面时,会将该页面的控制块放到flush链表的头部。也就是说,flush链表是按照页面第一次修改时间进行排序的。

当修改某个页面时,会更改页面对应控制块中的两个属性信息:

  • oldest_modification:第一次修改该页面,MTR结束时对应的lsn值
  • newest_modification:最新一次修改该页面,MTR结束时对应的lsn值

2.4 checkpoint

redo log的数量是不断增加的,但是redo日志文件组的容量是有限的,因此我们必须要循环利用redo日志文件组中的文件。

哪些文件可以被重用呢?显然当buffer pool中的脏页已经被刷新到磁盘时,这些脏页对应的redo日志就没有用了,这些日志占用的空间就可以复用。

那么我们怎么知道哪些脏页对应的redo log已经没有用了呢?InnoDB中设计了一个名为checkpoint_lsn的全局变量,表示当前系统中可以被覆盖的redo 日志总量是多少。这个变量的初始值也是8704。

checkpoint信息会被记录在redo日志文件的文件头中,我们把一次更新checkpoint信息的过程称之为“执行一次checkpoint”。

执行一次checkpoint的过程可以分成2个步骤:

  1. 计算当前系统中可以被覆盖的redo日志对应的lsn值最大是多少
  2. 将checkpoint_lsn、checkpoin_lsn对应的文件偏移量以及此次checkpoint编号写入日志文件管理信息中(checkpoint1或者checkpoint2中)。InnoDB中规定,checkpoint_no是偶数时写入checkpoint1中,是奇数就写入checkpoint2中。

执行完一次checkpoin,redo日志文件组中各个lsn值关系可能如下:

3 怎么恢复?

当系统崩溃重启动后,我们如何利用redo log来恢复没来得及刷新到磁盘的脏页数据呢?

3.1 确定起点

  • 对于lsn值小于checkpoint_lsn的redo日志来说,它们是可以被覆盖的,也就是说这些redo日志对应的脏页已经被刷新到磁盘中了,自然也就没有必要恢复它们
  • 对于lsn值大于checkpoint_lsn的redo日志,我们不能确定这些日志对应的脏页是否已经被刷盘了

因此恢复的起点就是checkpoint_lsn对应的redo日志开始恢复。

我们知道redo日志文件头中有两个checkpoint,我们只需要将两种中checkpoint_no读取出来,取值大者即可。

3.2 确定终点

redo日志文件是顺序写入的,写满了一个block再写下一个block。

普通block的log block header中有一个名为LOG_BLOCK_HDR_DATA_LEN属性,记录了该block中使用了多少空间。对于被填满的block,该值为512。如果某block该值小于512,那么该block就是崩溃恢复中需要扫描的最后一个block。

3.3 恢复数据

一种直接的方式就是:从起点到终点,按顺序恢复每一条redo 日志记录的信息。

不过InnoDB在此基础上进行了一些优化:

  • 使用哈希表

    根据redo日志的space ID和page number属性计算哈希值,把具有相同表空间id和页号的日志放到一起,按照生成顺序链接,如下图所示:

    这样可以一次性修复一整个页面,避免多次读取页面的随机IO,加快恢复速度。

  • 跳过已经刷新到磁盘中的页面

    对于lsn不小于checkpoint_lsn的redo 日志对应的在那个也也是有可能已经刷新到磁盘了(最后一次checkpoint后执行过刷新脏页的操作)。对于这些日志,崩溃恢复时,就没有必要再次进行恢复了。

    那么如何判断这些页面已经更新过了呢?

    在数据页的FileHeader中有一个称为FIL_PAGE_LSN属性,该属性记载了最近一次修改页面时对应的lsn值(即flush链表控制块中的newest_modification值)。如果该值大于checkpoint_lsn,说明这些页面在最后一次checkpoint之后,被刷新到磁盘,也就不需要被恢复了。这进一步提高了崩溃恢复的速度。

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

请我喝杯咖啡吧~

支付宝
微信