redo日志
本文为阅读《MySql是怎样运行的》的笔记,供大家参考。
文章目录
- redo日志
- 概述
- redo数据结构
- 通用结构
- redo日志类型
- redo日志组
- Mini-Transaction
- redo日志写入内存
- redo log buffer
- redo log block
- redo log 写入
- redo日志写入磁盘
- redo日志刷盘时机
- redo日志文件组
- redo日志文件格式
- Log Sequeue Number
- 全局LSN
- flushed_to_disk_lsn
- flush链表中的LSN
- checkpoint
- innodb_flush_log_at_trx_commit
- 崩溃恢复
- 恢复的范围
- 恢复过程
概述
InnoDB是以页为单位来管理存储空间的。在真正访问页面之前,需要把在磁盘上的页缓存到内存中的Buffer Pool之后才可以访问。所有的变更都必须先更新缓冲池中的数据,然后缓冲池中的脏页会以一定的频率被刷入磁盘,通过缓冲池来优化CPU和磁盘之间的鸿沟,这样就可以保证整体的性能不会下降太快。
但是如果我们只在内存的 Buffer Pool 中修改了页面,假设在事务提交后突然发生了某个故障,导致内存中的数据都失效了,那么这个已经提交了的事务对数据库中所做的更改也就跟着丢失了,这是我们所不能忍受的。
若每次提交事务之前都把所有事务的修改都刷入磁盘也可以解决持久性的问题,但是会很慢很消耗资源,这就有了redo日志出现。
我们没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把修改了哪些东西记录一下就好,比方说某个事务将系统表空间中的第100号页面中偏移量为1000处的那个字节的值 1 改成 2 我们只需要记录一下:将第0号表空间的100号页面的偏移量为1000处的值更新为 2
我们在事务进行时把上述日志加载到磁盘中,在事务提交过程中若发生宕机,则重启后只需要对着日志执行一遍满足持久性。
好处:
- 占用空间小
- 减少数据页的刷盘次数
特点:
- 顺序写入
- 事务执行过程中,redo log不断记录
- redo日志是存储引擎层产生的
redo数据结构
通用结构
各个部分的详细释义如下:
type :该条 redo 日志的类型。在 MySQL 5.7.21 这个版本中, InnoDB一共为 redo 日志设计了53种不同的类型。
space ID :表空间ID。
page number :页号。
data :该条 redo 日志的具体内容。
redo日志类型
简单类型: 有时候,一个条语句所做的修改比较少,可以用将x表空间的y页面的偏移量为z的值更新为n
这种很轻松的记录下来:
我们知道,当一张表没有主键时,mysql会自动为其维护一个隐藏的类似主键的row_id
mysql服务会在内存中维护一个全局变量,每当某个没有主键的表添加一个数据时,会使用这个全局变量为row_id赋值,并在内存中+1
每当这个变量变为256的倍数时,会将其刷盘至系统表空间的7号页中(表空间的前几张表为固定用途的表)的MaxRowID的属性中。
当系统重启时,会将MaxRowID加载到内存中,并将其+256
这个MaxRowID为8个字节,因此当某事务向某个包含表插入一条 row_id为256倍数的记录时,就会向系统表空间页号为7的页面的相应偏移量处写入8个字节。
我们知道,写入这个字段时其实是先将页面加载至内存中修改,因此需要为这条修改加一条redo日志,以便宕机后能恢复修改至磁盘。
InnoDB把这种极其简单的 redo 日志称之为 物理日志
,并且根据在页面中写入数据的多少划分了几种不同的 redo 日志类型:
- MLOG_1BYTE ( type 字段对应的十进制数字为 1 ):表示在页面的某个偏移量处写入1个字节的 redo 日志类型。
- MLOG_2BYTE ( type 字段对应的十进制数字为 2 ):表示在页面的某个偏移量处写入2个字节的 redo 日志类型。
- MLOG_4BYTE ( type 字段对应的十进制数字为 4 ):表示在页面的某个偏移量处写入4个字节的 redo 日志类型。
- MLOG_8BYTE ( type 字段对应的十进制数字为 8 ):表示在页面的某个偏移量处写入8个字节的 redo 日志类型。
- MLOG_WRITE_STRING ( type 字段对应的十进制数字为 30 ):表示在页面的某个偏移量处写入一串数据。
MLOG_1BYTE 、 MLOG_2BYTE 、 MLOG_4BYTE 类型的 redo 日志结构和 MLOG_8BYTE 的类似,只不过具体数据中包含对应个字节的数据罢了。
而MLOG_WRITE_STRING类型表示写入一串数据,但是因为不能确定写入的具体数据占用多少字节,所以需要在日志结构中添加一个 len 字段:
只要将MLOG_WRITE_STRING类型的redo日志的len字段填充上1、2、4、8这些数字,就可以分别替代MLOG_1BYTE、MLOG_2BYTE、MLOG_4BYTE、MLOG_8BYTE这些类型的redo日志,但是因为省空间,能不写len字段就不写len字段,省一个字节算一个字节。
**复杂类型:**有时候,一条语句会添加、修改:多条数据;多个页面、一个页面的不同位置的数据。
有时执行一条语句会修改很多页面,也包括系统数据页面和用户数据页面(也就是索引的B+树数据)。一条insert语句,可能会更新MaxRowID的值,也会向b+树种添加数据:表中有几个索引就要更新多少个树;对于具体某个B+树来说,即可能更新叶子节点页面,也可能同时更新非叶子节点页面,还可能页分裂。
而且数据页中除了用户数据外还可能需要更新文件头、页面头等系统数据。比如 PAGE_N_DIR_SLOTS 表示的槽数量可能会更改, PAGE_HEAP_TOP代表的还未使用的空间最小地址可能会更改, PAGE_N_HEAP 代表的本页面中的记录数量可能会更改等。还要更新维护页面单链表的下一个节点的指针指向。
如果在每个修改的地方都存储一条redo log,占用的空间就太大了。而如果把从第一处修改到最后一处修改中间的所有数据都记录为一条redo log又太浪费。
因此,innodb提出来一些复杂类型的redo log:
- MLOG_REC_INSERT (对应的十进制数字为 9 ):表示插入一条使用非紧凑行格式的记录时的 redo 日志类型。
- MLOG_COMP_REC_INSERT (对应的十进制数字为 38 ):表示插入一条使用紧凑行格式的记录时的 redo 日志类型。
Redundant是一种比较原始的行格式,它就是非紧凑的。而Compact、Dynamic以及Compressed行格式是较新的行格式,它们是紧凑的(占用更小的存储空间)。
- MLOG_COMP_PAGE_CREATE ( type 字段对应的十进制数字为 58 ):表示创建一个存储 紧凑行格式记录的页面的 redo 日志类型。
- MLOG_COMP_REC_DELETE ( type 字段对应的十进制数字为 42 ):表示删除一条使用 紧凑行格式记录的redo 日志类型。
- MLOG_COMP_LIST_START_DELETE ( type 字段对应的十进制数字为 44 ):表示从某条给定记录开始删除页面中的一系列使用紧凑行格式记录的类型。
- MLOG_COMP_LIST_END_DELETE ( type 字段对应的十进制数字为 43 ):表示删除一系列记录直到 MLOG_COMP_LIST_END_DELETE 类型的 redo 日志对应的记录为止。
有时候我们会有删除索引列的值在某个区间范围内的所有记录的需求,这时候如果我们每删除一条记录就写一条redo日志的话,效率可能有点低,所以提出MLOG_COMP_LIST_START_DELETE和MLOG_COMP_LIST_END_DELETE类型的redo日志,可以很大程度上减少redo日志的条数。
- MLOG_ZIP_PAGE_COMPRESS ( type 字段对应的十进制数字为 51 ):表示压缩一个数据页的 redo 日志类型
…等等等
这些redo日志微观意义上已经不是 将x表空间的y页面的偏移量为z的值更新为n
这种redo日志了,但是也都指明了对哪个表空间的哪个页进行了何种修改
。只是在系统奔溃重启时 ,并不能直接根据这些日志里的记载,将页面内的某个偏移量处恢复成某个数据,而是需要调用一些事先准备好的函数,执行完这些函数后才可以将页面恢复成系统奔溃前的样子。
如果不是为了写一个解析 redo 日志的工具或者自己开发一套 redo 日志系统的话,那就没必要把 InnoDB 中的各种类型的 redo 日志格式都研究的透透的, 只需要知道:redo日志会把事务在执行过程中对数据库所做的所有修改都记录下来,在之后系统奔溃重启后可以把事务所做的任何修改都恢复出来。
redo日志组
乐观插入:直接向叶子节点中插入一条数据,而无需进行页分裂。
悲观插入:要插入的叶子节点刚好满了,需要新创建一个页面,并且把一部分数据剪切过去,再进行插入,同时非叶子节点也需要插入一条目录指向新创建的节点
在悲观插入过程中,新的页面已经分配好了,数据也复制过去了,新的记录也插入到页面中了,可是没有向非叶子节点中插入一条目录项记录 ,这个插入过程就是不完整的,这样会形成一棵不正确的 B+ 树。
在系统奔溃重启时恢复崩溃前的状态,如果在悲观插入的过程中只记录了一部分 redo日志,那么在系统奔溃重启时会将索引对应的 B+ 树恢复成一种不正确的状态, 所以innodb规定在执行这些需要保证原子性的操作时必须以 组
对于多条日志为一组的情况
redo日志划分组,会在该组中的最后一条 redo 日志后边加上一条特殊类型的 redo 日志,该类型名称为 MLOG_MULTI_REC_END , type 字段对应的十进制数字为 31 ,该类型的 redo 日志结构很简单,只有一个 type 字段 。
所以某个需要保证原子性的操作产生的一系列 redo 日志必须要以一个类型为 MLOG_MULTI_REC_END 结尾 。这样在系统奔溃重启进行恢复时,只有当解析到类型为 MLOG_MULTI_REC_END 的 redo 日志,才认为解析到了一组完整的 redo 日志,才会进行恢复。否则的话直接放弃前边解析到的 redo 日志。
对于单条日志为一组的情况
redo 日志的类型有几十种,是小于 127 这个数字的,也就是说我们用7个比特位就足以包括所有的 redo 日志类型,而 type 字段是占用1个字节的,也就是说我们可以省出来一个比特位用来表示该需要保证原子性的操作只产生单一的一条redo 日志。
如果 type 字段的第一个比特位为 1 ,代表该需要保证原子性的操作只产生了单一的一条 redo 日志,否则表示该需要保证原子性的操作产生了一系列的 redo 日志。
Mini-Transaction
对底层页面中的一次原子访问的过程称之为一个 Mini-Transaction ,简称 mtr
一次mtr会产生一组redo log
一个事务可以包含若干条语句,每一条语句其实是由若干个 mtr 组成,每一个 mtr 又可以包含若干条 redo 日志
redo日志写入内存
redo log buffer
在服务器启动时Mysql就向操作系统申请了一大片称之为redo log buffer
的连续内存空间,翻译成中文就是 redo日志缓冲区
,我们也可以简称为 log buffer 。
这片内存空间被划分成若干个连续的 redo log block 。
我们可以通过启动参数 innodb_log_buffer_size 来指定 log buffer 的大小,在 MySQL 5.7.21 这个版本中,该启动参数的默认值为 16MB 。
redo log block
redo日志储存的数据结构。本质上是大小为 512字节的页,被称为block 。
真正的 redo 日志都是存储到占用 496 字节大小的 log block body 中,图中的 log block header 和 log blocktrailer 存储的是一些管理信息。
其中 log block header 的几个属性的意思分别如下:
- LOG_BLOCK_HDR_NO :每一个block都有一个大于0的唯一标号,本属性就表示该标号值。
- LOG_BLOCK_HDR_DATA_LEN :表示block中已经使用了多少字节,初始值为 12 (因为 log block body 从第12个字节处开始)。随着往block中写入的redo日志越来也多,本属性值也跟着增长。如果 log block body已经被全部写满,那么本属性的值被设置为 512 。
- LOG_BLOCK_FIRST_REC_GROUP :一条 redo 日志也可以称之为一条 redo 日志记录( redo log record ),一个 mtr 会生产多条 redo 日志记录,这些 redo 日志记录被称之为一个 redo 日志记录组( redo logrecord group )。 LOG_BLOCK_FIRST_REC_GROUP 就代表该block中第一个 mtr 生成的 redo 日志记录组的偏移量(其实也就是这个block里第一个 mtr 生成的第一条 redo 日志的偏移量)。
- LOG_BLOCK_CHECKPOINT_NO :表示所谓的 checkpoint 的序号, checkpoint 是我们后续内容的重点,现在先不用清楚它的意思,稍安勿躁。
log block trailer 中属性的意思如下:
- LOG_BLOCK_CHECKSUM :表示block的校验值,用于正确性校验,我们暂时不关心它。
redo log 写入
向 log buffer 中写入 redo 日志的过程是顺序的,也就是先往前边的block中写,当该block的空闲空间用完之后再往下一个block中写。
当往 log buffer 中写入 redo 日志时,就需要知道写在哪个block 的哪个偏移量处,所以 InnoDB提供了一个 buf_free全局变量,该变量指明后续写入的 redo 日志应该写入到 log buffer 中的哪个位置。
我们知道一个 mtr 执行过程中可能产生若干条 redo 日志,这些 redo 日志是一个不可分割的组,所以并不是每生成一条 redo 日志,就将其插入到 log buffer 中,而是每个 mtr 运行过程中产生的日志先暂时存到一个地方,当该 mtr 结束的时候,将过程中产生的一组 redo 日志再全部复制到 log buffer 中。
假设有两个名为 T1 、 T2 的事务,每个事务都包含2个 mtr :
- 事务 T1 的两个 mtr 分别称为 mtr_T1_1 和 mtr_T1_2 。
- 事务 T2 的两个 mtr 分别称为 mtr_T2_1 和 mtr_T2_2 。
不同的事务可能是并发执行的,所以 T1 、 T2 之间的 mtr 可能是交替执行的。每当一个 mtr 执行完成时,伴随该 mtr 生成的一组 redo 日志就需要被复制到 log buffer 中,也就是说不同事务的 mtr 可能是交替写入 log buffer 的 :
不同的 mtr 产生的一组 redo 日志占用的存储空间可能不一样,有的 mtr 产生的redo 日志量很少,比如 mtr_t1_1 、 mtr_t2_1 就被放到同一个block中存储,有的 mtr 产生的 redo 日志量非常大,比如 mtr_t1_2 产生的 redo 日志占用了3个block来存储。
redo日志写入磁盘
redo日志刷盘时机
- log buffer 空间不足时
如果当前写入 log buffer 的redo 日志量已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。 - 事务提交时
在事务提交时可以不把修改过的数据页面刷新到磁盘,但是为了保证持久性,必须要把对应的 redo 日志刷新到磁盘。 - 后台线程不停的刷刷刷
后台线程大约每秒都会刷新一次 log buffer 中的 redo 日志到磁盘。 - 正常关闭服务器时
- checkpoint 时
- 其他的一些情况…
redo日志文件组
MySQL 的数据目录下默认有两个名为 ib_logfile0 和ib_logfile1 的文件, log buffer 中的日志默认情况下就是刷新到这两个文件中(可以通过启动参数来调节 )
磁盘上的 redo 日志文件不只一个,而是以一个 日志文件组 的形式出现的。这些文件以 ib_logfile[数字]
的形式进行命名。
在将 redo 日志写入 日志文件组 时,是从 ib_logfile0 开始写,如果0写满了,就接着1 写,1 写满了就去写2 。直至最后一个文件写满了,会回到0开始写。
这个在文件组中循环写的方式会涉及到后面说的check point。
redo日志文件格式
我们知道log buffer 是一片连续的内存空间,被划分成了若干个 512 字节大小的 block。redo 日志文件也是由若干个 512 字节大小的block组成。
redo 日志文件组中的每个文件大小、格式都一样,由两部分组成:前2048个字节,也就是前4个block是用来存储一些管理信息的,而2048个字节之后才是储存redo日志的,与log buffer保持一致,都是常规的log block。所以 循环 使用redo日志文件,其实是从每个日志文件的第2048个字节开始算的。
而前2048个字节所对应的4个 block相对特殊:
- log file header :描述该 redo 日志文件的一些整体属性
LOG_HEADER_FORMAT:redo 日志的版本,在 MySQL5.7.21 中该值永远为1
LOG_HEADER_PAD1:做字节填充用的,没有实际意义
LOG_HEADER_START_LSN:标记本 redo 日志文件开始的LSN值,也就是文件偏移量为2048字节初对应的LSN值。
LOG_HEADER_CREATOR:一个字符串,标记本 redo 日志文件的创建者是谁。正常运行时该值为 MySQL 的版本号,比如: “MySQL 5.7.21” ,使用mysqlbackup 命令创建的 redo 日志文件的该值为 “ibbackup” 和创建时间。
LOG_BLOCK_CHECKSUM本block的校验值,所有block都有,我们不关心
- checkpoint1 :记录关于 checkpoint 的一些属性
LOG_CHECKPOINT_NO:服务器做 checkpoint 的编号,每做一次 checkpoint ,该值就加1。
LOG_CHECKPOINT_LSN:服务器做 checkpoint 结束时对应的 LSN 值,系统奔溃恢复时将从该值开始
LOG_CHECKPOINT_OFFSET:上个属性中的 LSN 值在 redo 日志文件组中的偏移量
LOG_CHECKPOINT_LOG_BUF_SIZE:服务器在做 checkpoint 操作时对应的 log buffer 的大小
LOG_BLOCK_CHECKSUM:本block的校验值,所有block都有,我们不关心
Log Sequeue Number
全局LSN
自MySql开始运行,就不断的生成 redo 日志。就像人的年龄一样不断的递增,永远不可能缩减了。 InnoDB 为记录已经写入的 redo 日志量,设计了一个全局变量Log Sequeue Number (日志序列号) ,简称 lsn 。初始的 lsn 值为 8704 (一条 redo 日志也没写入时,lsn 的值为 8704 )。
我们知道,实际redo log写入时会写入到block body中,但是lsn记录时会同时记录block header、trailer的占用。
- 系统第一次启动后初始化 log buffer 时, buf_free 就会指向第一个 block 的偏移量为12字节( log block header 的大小)的地方,那么 lsn值也会跟着增加12
- 如果某个 mtr 产生的一组 redo 日志占用空间较少(假设为200字节),待插入的block剩余空间能容纳时, lsn 增长的量就是该 mtr 生成的日志占用的字节数:
- 如果某个 mtr 产生的一组 redo 日志占用空间较大,待插入的block剩余空间不足以容纳时, lsn 增长的量就是该 mtr 生成的 redo 日志占用的字节数加上额外占用的block header 和 block trailer 的字节数
每一组由mtr生成的redo日志都有一个唯一的LSN值与其对应,LSN值越小,说明redo日志产生的越早。
flushed_to_disk_lsn
redo 日志是先写到 log buffer 中,之后才会被刷新到磁盘上的 redo 日志文件。 InnoDB 提出了全局变量buf_next_to_write ,标记当前 log buffer 中已经有哪些日志被刷新到磁盘中了。 (可以理解为磁盘的lsn,当然,不是存储在磁盘中的lsn)
当有新的 redo 日志写入到 log buffer 时,首先 lsn 的值会增长,但 flushed_to_disk_lsn 不变。当不断有 log buffer 中的日志被刷新到磁盘上时,flushed_to_disk_lsn 的值也跟着增长。如果两者的值相同时,说明log buffer中的所有redo日志都已经刷新到磁盘中了。
应用程序向磁盘写入文件时其实是先写到操作系统的缓冲区中去,如果某个写入操作要等到操作系统确认已经写到磁盘时才返回,那需要调用一下操作系统提供的fsync函数。其实只有当系统执行了fsync函数后,flushed_to_disk_lsn的值才会跟着增长,当仅仅把log buffer中的日志写入到操作系统缓冲区却没有显式的刷新到磁盘时,另外的一个称之为write_lsn的值跟着增长。不过为了大家理解上的方便,我们在讲述时把flushed_to_disk_lsn和write_lsn的概念混淆了起来
flush链表中的LSN
flush链表是由脏页组成的一个链表,脏页是在内存中已经修改了的但是还没有刷到磁盘中去的页面。(事务没有提交时也会在脏页中存在修改的页面)
当第一次修改某页面时,会将这个页面插入到flush链表的头部,之后再有语句修改这个页面就不会再插入了。(flush是按照页面的首次修改时间排序的)
因此链表控制块中有两个字段会保存首次修改时间和最后一次修改时间:
- oldest_modification :第一次修改该页面的mtr 开始时对应的 lsn 值写入这个属性。
- newest_modification :每次修改该页面都会把当时的mtr对应的lsn写入
checkpoint
redo 日志文件组容量是有限的 , 所以只能选择循环插入log,但是这会导致最后写的redo log与最开始写的log 追尾。
但事实上,我们也不需要很早之前的redo log也保存起来,因为redo log的作用就是防止事务提交时宕机,导致有内存中的脏页
没有刷新到磁盘中去。如果某些redo log 对应的脏页已经刷新到磁盘中去了,那这些脏页也就没用了。
所以,判断某些redo log是否可以被覆盖,只需要看它所对应的脏页有没有被刷新到磁盘中去。
InnoDB 提出了一个全局变量 checkpoint_lsn 来代表当前系统中可以被覆盖的 redo 日志总量是多少,这个变量初始值也是8704 。
比方说现在页a 被刷新到了磁盘, 其对应的mtr生成的 redo 日志就可以被覆盖了,所以我们可以进行一个增加checkpoint_lsn 的操作,我们把这个过程称之为做一次 checkpoint 。
做一次 checkpoint可以分为两个步骤:
- 计算一下当前系统中可以被覆盖的 redo 日志对应的 lsn 值最大是多少。
只要是lsn 小于 脏页链表中最早一个控制块的oldest_modification的redo日志,都是可以覆盖的。
我们就把该脏页的 oldest_modification 赋值给 checkpoint_lsn 即可。 - 将 checkpoint_lsn 和对应的 redo 日志文件组偏移量以及此次 checkpint 的编号写到日志文件的管理信息(即checkpoint1、checkpoint2 )中。
InnoDB维护了一个目前系统做了多少次 checkpoint 的变量 checkpoint_no ,每做一次checkpoint ,该变量的值就加1。
计算得到该 checkpoint_lsn 在 redo 日志文件组中对应的偏移量 checkpoint_offset ,然把这三个值都写到 redo 日志文件组的管理信息中。
InnoDB规定,当 checkpoint_no 的值是偶数时,就写到 checkpoint1 中,是奇数时,就写到checkpoint2 中。
innodb_flush_log_at_trx_commit
如果对持久性
要求没有那么高,可以选择修改这个变量的值:
- 0 :当该系统变量值为0时,表示在事务提交时不立即向磁盘中同步 redo 日志,这个任务是交给后台线程做的。
这样很明显会加快请求处理速度,但是如果事务提交后服务器挂了,后台线程没有及时将 redo 日志刷新到
磁盘,那么该事务对页面的修改会丢失。 - 1 :当该系统变量值为1时,表示在事务提交时需要将 redo 日志同步到磁盘,可以保证事务的 持久性 。 1也是 innodb_flush_log_at_trx_commit 的默认值。
- 2 :当该系统变量值为2时,表示在事务提交时需要将 redo 日志写到操作系统的缓冲区中,但并不需要保证将日志真正的刷新到磁盘。
这种情况下如果数据库挂了,操作系统没挂的话,事务的 持久性 还是可以保证的,但是操作系统也挂了的话,那就不能保证 持久性 了。
崩溃恢复
恢复的范围
redo日志就是为了服务挂掉之后重启恢复而存在的,首先要确定需要恢复的redo log的范围。
开始:
checkpoint_lsn之前的日志都可以被覆盖,也就是说其对应的脏页都被刷新到磁盘中了,对于checkpoint_lsn之后的日志,其脏页可能被刷盘了也可能没有(脏页刷新了但是还没来得及做checkpoint),所以需要恢复的redo log的起点就是lsn值为checkpoint_lsn的redo日志。
结束:
我们知道,redo log是存储在log buffer中的block中的,而且是顺序存储的,所以只需要找到没有储存满的block,那么就找到了最后一条redo log。
可以通过普通block的 log block header 中的 LOG_BLOCK_HDR_DATA_LEN 来确认。block大小为512字节,所以小于512的block就是最后一个了。
恢复过程
现在可以按照 redo 日志的顺序依次扫描checkpoint_lsn 之后的各条redo日志,按照日志中记载的内容将对应的页面恢复出来。不过InnoDB还是想了一些办法加快这个恢复的过程:
- 使用哈希表
根据redo 日志中的space ID 和 page number计算哈希,也就是表空间id和页面id,如果是同一页面的,则根据时间顺序,使用链表排序起来。
之后就可以遍历hash table进行恢复,因为对同一个页面的修改是放在一起的,所以是顺序IO,可以加快恢复速度。恢复的时候也是根据时间顺序来的,这样就不会出现 redo日志是先添加一条再删除一条,而恢复的时候是先删除再添加了。 - 跳过已经刷新到磁盘的页面
对于checkpoint_lsn之后的日志,可能在最近一次checkpoint之后还有脏页被刷新到磁盘中了。那在恢复时也就没有必要根据 redo 日志的内容修改该页面了。 在页面文件头中,有一个 FIL_PAGE_LSN ,该属性记载了最近一次修改页面时对应的 lsn 值 ,即脏页链表控制块中的newest_modification。
如果在 checkpoint 之后有脏页被刷新到磁盘中,那么该页对应的FIL_PAGE_LSN 代表的 lsn 值肯定大于 checkpoint_lsn 的值,凡是符合这种情况的页面就不需要恢复了。也就是说,lsn值小于 FIL_PAGE_LSN 的redo日志就不需要执行了。