吊打面试官之 redo log 详解
不知道大家是否还记得事务的四大特性,现在我们回忆一下吧,持久性、一致性、隔离性、原子性。没错,我们今天讲的就是持久性。
一、啥是 redo log
我们想象有这么一个生产环境,就是如果我们只在内存的 Buffer Pool 中修改了页面,事务提交后突然发生了某个故障,导致内存中的数据都失效了,那么这个已经提交了的事务对数据库中所做的更改也就跟着丢失了,这你能忍?反正我是不能忍。🙂🙂🙂 这就是持久性遭到了破坏,那我们如何保证持久性呢? 有同学可能说每次事务提交之前把该事务所修改的所有页面都刷新到磁盘。但是,你有没有想过这样做有问题呢?
-
刷新一个完整的数据页太浪费了
有时候我们仅仅修改了某个页面中的一个字节,但是我们知道在 InnoDB 中是以页为单位来进行磁盘IO 的,也就是说我们在该事务提交时不得不将一个完整的页面从内存中刷新到磁盘,我们又知道一个页面默认是 16KB 大小,只修改一个字节就要刷新 16KB 的数据到磁盘上显然是太浪费了
-
随机 IO 比较慢
一个事务可能包含很多条 SQL 语句,而这些语句可能对 Buffer Pool 中不相邻的数据页进行操作。当把该事务修改过的数据页刷新到磁盘时会产生很多随机 IO,我们知道随机 IO 是比顺序 IO 慢的
我们还记得我们的目的是啥吗?不就是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来。所以我们其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把修改了哪些东西记录一下就好。那用什么东东来记录呢,此时我们的 redo log 就登场了。
redo log 顾名思义,就是重做日志,我们只需要将对数据页做了哪些修改记录到 redo log 就行了,当系统崩溃了,重启之后只要按照 redo log 所记录的步骤重新更新一下数据页,那么该事务对数据库中所做的修改又可以被恢复出来,满足持久性。那为啥用 redo log 呢?
- redo 日志占用的空间非常小
存储表空间ID、页号、偏移量以及需要更新的值所需的存储空间是很小的 - redo 日志是顺序写入磁盘的
在执行事务的过程中,每执行一条语句,就可能产生若干条 redo 日志,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序IO,速度比随机 IO 更快
讲了啥是 redo log,我们现在看看真实的 redo log 到底长啥样吧
二、redo log 格式
redo log 的格式:
- type :该条 redo 日志的类型。
- space ID :表空间 ID。
- page number :页号。
- data :该条 redo 日志的具体内容。
这里 redo log 日志的类型比较多,我在这里举两个例子吧:
还有比较复杂的 redo log 的类型,我这里就不做详述了,感兴趣的同学可以自行去学习 🤣🤣🤣
三、Mini-Transaction 的概念
MySQL 把对底层页面中的一次原子访问的过程称之为一个 Mini-Transaction ,简称 mtr ,比如向某个索引对应的 B+ 树中插入一条记录的过程就是一个 Mini-Transaction 。通过上边的叙述我们也知道,一个所谓的 mtr 可以包含一组 redo 日志,在进行奔溃恢复时这一组 redo 日志作为一个不可分割的整体
一个事务可以包含若干条语句,每一条语句其实是由若干个 mtr 组成,每一个 mtr 又可以包含若干条 redo 日志,画个图表示它们的关系就是这样:
四、redo日志的写入过程
1、redo日志的写入过程
redo 日志也是需要存储的啊,所以 MySQL 把它放在了大小为 512 字节的页中,但是我们把用来存储 redo 日志的页称为 block,画一个图来说明更加形象一点:
真正的 redo 日志都是存储到占用 496 字节大小的 log block body 中,图中的 log block header 和 log block trailer 存储的是一些管理信息,那这些管理信息是啥呢,我们画一个图说明:
LOG_BLOCK_HDR_NO
:每一个 block 都有一个大于 0 的唯一标号,本属性就表示该标号LOG_BLOCK_HDR_DATA_LEN
:表示 block 中已经使用了多少字节,初始值为 12 (因为 log block body 从第12个字节处开始)。随着往 block 中写入的 redo 日志越来也多,本属性值也跟着增长。如果 log block body 已经被全部写满,那么本属性的值被设置为 512LOG_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 的序号LOG_BLOCK_CHECKSUM
:表示 block 的校验值,用于正确性校验
2、redo日志缓冲区
为了解决磁盘速度过慢的问题而引入了 Buffer Pool 。同理,写入 redo 日志时也不能直接直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为 redo log buffer 的连续内存空间,也叫 redo日志缓冲区 ,简称为 log buffer 。这片内存空间被划分成若干个连续的 redo log block ,如图:
小贴士:我们可以通过启动参数
innodb_log_buffer_size
来指定 log buffer 的大小,在 MySQL 5.7.21 这个版本中,该启动参数的默认值为 16MB 。
3、redo日志写入log buffer
向 log buffer 中写入 redo 日志的过程是顺序的,也就是先往前边的block中写,当该block的空闲空间用完之后再往下一个block中写。当我们想往 log buffer 中写入 redo 日志时,第一个遇到的问题就是应该写在哪个 block 的哪个偏移量处,因此 MySQL 提供了一个称之为 buf_free 的全局变量,该变量指明后续写入的 redo 日志应该写入到 log buffer 中的哪个位置,如图所示:
我们前边说过一个 mtr 执行过程中可能产生若干条 redo 日志,这些 redo 日志是一个不可分割的组,所以其实并不是每生成一条 redo 日志,就将其插入到 log buffer 中,而是每个 mtr 运行过程中产生的日志先暂时存到一个地方,当该 mtr 结束的时候,将过程中产生的一组 redo 日志再全部复制到 log buffer 中。我们现在假设有两个名为 T1 、 T2 的事务,每个事务都包含2个 mtr ,我们给这几个 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日志文件
1、redo日志刷盘时机
mtr 运行过程中产生的一组 redo 日志在 mtr 结束时会被复制到 log buffer 中,既然在 log buffer 中,我们就需要在一定时机把它们刷新到磁盘里,比如:
-
log buffer 空间不足时
log buffer 的大小是有限的,如果不停的往这个有限大小的 log buffer 里塞入日志,很快它就会被填满。MySQL 认为如果当前写入 log buffer 的 redo 日志量已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。小贴士:log buffer 的大小通过系统变量
innodb_log_buffer_size
指定 -
事务提交时
我们前边说过之所以使用 redo 日志主要是因为它占用的空间少,还是顺序写,在事务提交时可以不把修改过的 Buffer Pool 页面刷新到磁盘,但是为了保证持久性,必须要把修改这些页面对应的 redo 日志刷新到磁盘。 -
后台线程不停的刷
后台有一个线程,大约每秒都会刷新一次 log buffer 中的 redo 日志到磁盘。 -
正常关闭服务器时
-
做所谓的 checkpoint 时
2、redo日志文件组
MySQL 的数据目录(使用 SHOW VARIABLES LIKE 'datadir' 查看)下默认有两个名为 ib_logfile0
和
ib_logfile1
的文件, log buffer 中的日志默认情况下就是刷新到这两个磁盘文件中。如果我们对默认的
redo 日志文件不满意,可以通过下边几个启动参数来调节:
innodb_log_group_home_dir
该参数指定了 redo 日志文件所在的目录,默认值就是当前的数据目录。innodb_log_file_size
该参数指定了每个 redo 日志文件的大小,在 MySQL 5.7.21 这个版本中的默认值为 48MB ,innodb_log_files_in_group
该参数指定 redo 日志文件的个数,默认值为2,最大值为100。
从上边的描述中可以看到,磁盘上的 redo 日志文件不只一个,而是以一个 日志文件组 的形式出现的。这些文件以 ib_logfile[数字] ( 数字 可以是 0 、 1 、 2 ...)的形式进行命名。在将 redo 日志写入 日志文件组 时,是从 ib_logfile0 开始写,如果 ib_logfile0 写满了,就接着 ib_logfile1 写,同理, ib_logfile1 写满了就去写 ib_logfile2 ,依此类推。如果写到最后一个文件该咋办?那就重新转到 ib_logfile0 继续写,所以整个过程如下图所示:
总共的 redo 日志文件大小其实就是: innodb_log_file_size
× innodb_log_files_in_group
。
小贴士:如果采用循环使用的方式向redo日志文件组里写数据的话,那岂不是要追尾,也就是后写入的 redo 日志覆盖掉前边写的 redo日志 ?当然可能了!InnoDB 提出了 checkpoint 的概念
3、redo日志文件格式
redo 日志文件由若干个 512 字节大小的 block 组成。redo 日志文件组中的每个文件大小都一样,格式也一样,都是由两部分组成:
- 前 2048 个字节,也就是前 4 个 block 是用来存储一些管理信息的。
- 从第 2048 字节往后是用来存储 log buffer 中的 block 镜像的。
我们还是画一个图进行说明吧:
从图中可以看出所说的循环使用 redo 日志文件,其实是从每个日志文件的第 2048 个字节开始算
那这前4个 block 存的都是啥管理信息呢?让我们来揭开它神秘的面纱 😄😄😄
还是直接上图吧:
log file header :描述该 redo 日志文件的一些整体属性,看一下它的结构:
LOG_HEADER_FORMAT
(0,4字节):redo 日志的版本,在 MySQL5.7.21 中该值永远为 1LOG_HEADER_PAD1
(4,4字节):做字节填充用的,没什么实际意义LOG_HEADER_START_LSN
(8,8字节):标记本 redo 日志文件开始的 LSN 值,也就是文件偏移量为 2048 字节初对应的 LSN 值LOG_HEADER_CREATOR
(16,32字节):一个字符串,标记本 redo 日志文件的创建者是谁。正常运行时该值为 MySQL 的版本号,比如: "MySQL 5.7.21" ,使用 mysqlbackup 命令创建的 redo 日志文件的该值为 "ibbackup" 和创建时间LOG_BLOCK_CHECKSUM
(508,4字节):本 block 的校验值
checkpoint1 :记录关于 checkpoint 的一些属性,如上图:
LOG_CHECKPOINT_NO
(0,8字节):服务器做 checkpoint 的编号,每做一次 checkpoint ,该值就加 1LOG_CHECKPOINT_LSN
(8,8字节):服务器做 checkpoint 结束时对应的 LSN 值,系统奔溃恢复时将从该值开始LOG_CHECKPOINT_OFFSET
(16,8字节):上个属性中的 LSN 值在 redo 日志文件组中的偏移量LOG_CHECKPOINT_LOG_BUF_SIZE
(24,8字节):|服务器在做 checkpoint 操作时对应的 log buffer 的大小LOG_BLOCK_CHECKSUM
:本block的校验值
4、Log Sequeue Number
Log Sequeue Number 是一个全局变量 , 也叫做日志序列号,用来记录已经写入的 redo 日志量,初始值是 8704
我们知道在向 log buffer 中写入 redo 日志时不是一条一条写入的,而是以一个 mtr 生成的一组 redo 日志为单位进行写入的。而且实际上是把日志内容写在了 log block body 处。但是在统计 lsn 的增长量时,是按照实际写入的日志量加上占用的 log block header 和 log block trailer 来计算的。我们来看一个例子:
每一组由 mtr 生成的redo日志都有一个唯一的 LSN值 与其对应,LSN值 越小,说明 redo日志 产生的越早
5、flushed_to_disk_lsn
redo 日志是首先写到 log buffer 中,之后才会被刷新到磁盘上的 redo 日志文件。所以 InnoDB 提出了一个称之为 buf_next_to_write 的全局变量,标记当前 log buffer 中已经有哪些日志被刷新到磁盘中了。画个图表示就是这样:
lsn
是表示当前系统中写入的 redo 日志量,这包括了写到 log buffer 而没有刷新到磁盘的日志
flushed_to_disk_lsn
表示刷新到磁盘中的 redo 日志量的全局变量,该变量的值和初始的 lsn 值是相同的,都是 8704 。随着系统的运行, redo 日志被不断写入 log buffer ,但是并不会立即刷新到磁盘, lsn 的值就和 flushed_to_disk_lsn 的值拉开了差距,画一个图看看吧:
小总结:有新的 redo 日志写入到 log buffer 时,首先 lsn 的值会增长,但 flushed_to_disk_lsn
不变,
随后随着不断有 log buffer 中的日志被刷新到磁盘上, flushed_to_disk_lsn
的值也跟着增长。如果两者的值相同时,说明 log buffer 中的所有 redo日志 都已经刷新到磁盘中了
6、lsn值和redo日志文件偏移量的对应关系
因为 lsn 的值是代表系统写入的 redo 日志量的一个总和,一个 mtr 中产生多少日志, lsn 的值就增加多少(当然有时候要加上 log block header 和 log block trailer 的大小),这样 mtr 产生的日志写到磁盘中时,很容易计算某一个 lsn 值在 redo 日志文件组中的偏移量,如图:
初始时的 LSN 值是 8704 ,对应文件偏移量 2048 ,之后每个 mtr 向磁盘中写入多少字节日志, lsn 的值就增长多少。
7、flush链表中的LSN
我们要把在 mtr 执行过程中可能修改过的页面加入到 Buffer Pool 的 flush 链表中
当第一次修改某个缓存在 Buffer Pool 中的页面时,就会把这个页面对应的控制块插入到 flush链表 的头部,之后再修改该页面时由于它已经在 flush 链表中了,就不再次插入了。也就是说 flush 链表中的脏页是按照页面的第一次修改时间从大到小进行排序的。在这个过程中会在缓存页对应的控制块中记录两个关于页面何时修改的属性:
oldest_modification
:如果某个页面被加载到 Buffer Pool 后进行第一次修改,那么就将修改该页面的 mtr 开始时对应的 lsn 值写入这个属性。newest_modification
:每修改一次页面,都会将修改该页面的 mtr 结束时对应的 lsn 值写入这个属性。也就是说该属性表示页面最近一次修改后对应的系统 lsn 值。
我们画一个图康康吧:
flush 链表中的脏页按照修改发生的时间顺序进行排序,也就是按照 oldest_modification
代表的 LSN 值进行排序,被多次更新的页面不会重复插入到 flush 链表中,但是会更新 newest_modification
属性的值
六、checkpoint
我们的 redo 日志文件组容量是有限的,我们不得不选择循环使用 redo 日志文件组中的文件,但是这会造成最后写的 redo 日志与最开始写的 redo 日志 追尾 ,这时应该想到:redo日志只是为了系统奔溃后恢复脏页用的,如果对应的脏页已经刷新到了磁盘,也就是说即使现在系统奔溃,那么在重启后也用不着使用 redo 日志恢复该页面了,所以该 redo 日志也就没有存在的必要了,那么它占用的磁盘空间就可以被后续的
redo 日志所重用。也就是说:判断某些 redo 日志占用的磁盘空间是否可以覆盖的依据就是它对应的脏页是否已经刷新到磁盘里
假如一个 mtr 对应的页都被刷到了磁盘,那么它们对应的控制块就会从 flush链表 中移除。这样 mtr 生成的 redo 日志就没有用了,它们占用的磁盘空间就可以被覆盖掉了。InnoDB 提出了一个全局变量checkpoint_lsn
来代表当前系统中可以被覆盖的 redo 日志总量是多少,这个变量初始值也是 8704
比方说现在 一些页 被刷新到了磁盘, 其对应 mtr 生成的 redo 日志就可以被覆盖了,所以我们可以进行一个增加checkpoint_lsn
的操作,我们把这个过程称之为做一次 checkpoint 。做一次 checkpoint 其实可以分为两个步骤:
-
步骤一:计算一下当前系统中可以被覆盖的 redo 日志对应的 lsn 值最大是多少。
redo 日志可以被覆盖,意味着它对应的脏页被刷到了磁盘,只要我们计算出当前系统中被最早修改的脏页对应的
oldest_modification
值,那凡是在系统 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
,然后把这三个值都写到 reo 日志文件组的管理信息中。我们说过,每一个 redo 日志文件都有 2048 个字节的管理信息,但是上述关于checkpoint的信息只会被写到日志文件组的第一个日志文件的管理信息中。不过我们是存储到 checkpoint1 中还是 checkpoint2 中呢?InnoDB 规定,当checkpoint_no
的值是偶数时,就写到 checkpoint1 中,是奇数时,就写到checkpoint2 中。
我们还是画一个图吧:
其中 mtr_1 所修改的页面全部都刷入磁盘,所以它对应的 redo日志 可以被覆盖,mtr_2 所修改的页面还在 Buffer Pool 中,不能被覆盖,但是 mtr_2 对应的日志已经存储在磁盘中了
刷脏页:
我们在介绍 Buffer Pool 的时候说过,一般情况下都是后台的线程在对 LRU链表 和 flush链表 进行刷脏操作,这主要因为刷脏操作比较慢,不想影响用户线程处理请求。但是如果当前系统修改页面的操作十分频繁,这样就导致写日志操作十分频繁,系统 lsn 值增长过快。如果后台的刷脏操作不能将脏页刷出,那么系统无法及时做 checkpoint ,可能就需要用户线程同步的从 flush链表 中把那些最早修改的脏页( oldest_modification
最小的脏页)刷新到磁盘,这样这些脏页对应的 redo 日志就没用了,然后就可以去做 checkpoint 了。
查看系统中的各种LSN值:
我们可以使用 SHOW ENGINE INNODB STATUS 命令查看当前 InnoDB 存储引擎中的各种 LSN 值的情况,比如:
mysql> SHOW ENGINE INNODB STATUS\G
Log sequence number 124476971 # 代表系统中的 lsn 值,也就是当前系统已经写入的 redo 日志量,包括写入 log buffer 中的日志
buffer 中的日志
Log flushed up to 124099769 # 代表 flushed_to_disk_lsn 的值,也就是当前系统已经写入磁盘的 redo 日志量
Pages flushed up to 124052503 # 代表 flush链表 中被最早修改的那个页面对应的 oldest_modification 属性值
Last checkpoint at 124052494 #当前系统的 checkpoint_lsn 值
innodb_flush_log_at_trx_commit 的用法:
为了保证事务的 持久性 ,用户线程在事务提交时需要将该事务执行过程中产生的所有 redo 日志都刷新到磁盘上。这一条要求太狠了,会很明显的降低数据库性能。如果有的同学对事务的持久性要求不是那么强
烈的话,可以选择修改一个称为 innodb_flush_log_at_trx_commit
的系统变量的值,该变量有3个可选的值:
- 0 :当该系统变量值为0时,表示在事务提交时不立即向磁盘中同步 redo 日志,这个任务是交给后台线程做的。这样很明显会加快请求处理速度,但是如果事务提交后服务器挂了,后台线程没有及时将 redo 日志刷新到磁盘,那么该事务对页面的修改会丢失。
- 1 :当该系统变量值为1时,表示在事务提交时需要将 redo 日志同步到磁盘,可以保证事务的持久性。 1 也是
innodb_flush_log_at_trx_commit
的默认值。 - 2 :当该系统变量值为2时,表示在事务提交时需要将 redo 日志写到操作系统的缓冲区中,但并不需要保证将日志真正的刷新到磁盘。这种情况下如果数据库挂了,操作系统没挂的话,事务的持久性还是可以保证的,但是操作系统也挂了的话,那就不能保证持久性了。
七、崩溃恢复
1、确定恢复的起点
我们前边说过, checkpoint_lsn
之前的 redo 日志都可以被覆盖,也就是说这些 redo 日志对应的脏页都已经被刷新到磁盘中了,既然它们已经被刷盘,我们就没必要恢复它们了。对于 checkpoint_lsn
之后的 redo 日志,它们对应的脏页可能没被刷盘,也可能被刷盘了,我们不能确定,所以需要从 checkpoint_lsn 开始读取 redo 日志来恢复页面。当然, redo 日志文件组的第一个文件的管理信息中有两个 block 都存储了 checkpoint_lsn
的信息,我们当然是要选取最近发生的那次 checkpoint 的信息。衡量 checkpoint 发生时间早晚的信息就是所谓的 checkpoint_no
,我们只要把 checkpoint1 和 checkpoint2 这两个block中的 checkpoint_no
值读出来比一下大小,哪个的 checkpoint_no
值更大,说明哪个 block 存储的就是最近的一次 checkpoint 信息。这样我们就能拿到最近发生的 checkpoint 对应的 checkpoint_lsn
值以及它在 redo 日志文件组中的偏移量 checkpoint_offset
2、确定恢复的终点
redo 日志恢复的起点确定了,那终点是哪个呢?这个还得从block的结构说起。我们说在写 redo 日志的时候都是顺序写的,写满了一个block之后会再往下一个block中写:
普通 block 的 log block header 部分有一个称之为 LOG_BLOCK_HDR_DATA_LEN
的属性,该属性值记录了当前 block 里使用了多少字节的空间。对于被填满的block来说,该值永远为 512 。如果该属性的值不为 512 ,那么就是它了,它就是此次奔溃恢复中需要扫描的最后一个block。
3、怎么恢复
那肯定是根据 redo 日志恢复了,如图:
由于 redo 0 在 checkpoint_lsn 后边,恢时可以不管它。我们现在可以按照 redo 日志的顺序依次扫描
checkpoint_lsn 之后的各条 redo日志,按照日志中记载的内容将对应的页面恢复出来。这样没什么问题,不过 InnoDB 还是想了一些办法加快这个恢复的过程:
那就是使用 哈希表 😄😄😄
根据 redo 日志的 space ID 和 page number 属性计算出散列值,把 space ID 和 page number 相同的 redo日志 放到哈希表的同一个槽里,如果有多个 space ID 和 page number 都相同的 redo 日志,那么它们之间使用链表连接起来,按照生成的先后顺序链接起来的,如图所示:
之后就可以遍历哈希表,因为对同一个页面进行修改的 redo 日志都放在了一个槽里,所以可以一次性将一
个页面修复好(避免了很多读取页面的随机IO),这样可以加快恢复速度。另外需要注意一点的是,同一个
页面的 redo 日志是按照生成时间顺序进行排序的,所以恢复的时候也是按照这个顺序进行恢复,如果不按
照生成时间顺序进行排序的话,那么可能出现错误。比如原先的修改操作是先插入一条记录,再删除该条记
录,如果恢复时不按照这个顺序来,就可能变成先删除一条记录,再插入一条记录,这显然是错误的。
跳过已经刷新到磁盘的页面
我们前边说过, checkpoint_lsn
之前的 redo 日志对应的脏页确定都已经刷到磁盘了,但是 checkpoint_lsn
之后的 redo 日志我们不能确定是否已经刷到磁盘,主要是因为在最近做的一次 checkpoint 后,可能后台线程又不断的从 LRU链表 和 flush链表 中将一些脏页刷出 Buffer Pool 。这些在 checkpoint_lsn
之后的 redo 日志,如果它们对应的脏页在奔溃发生时已经刷新到磁盘,那在恢复时也就没有必要根据 redo 日志的内容修改该页面了。
那在恢复时怎么知道某个 redo 日志对应的脏页是否在奔溃发生时已经刷新到磁盘了呢?这还得从页面的结
构说起,我们前边说过每个页面都有一个称之为 File Header 的部分,在 File Header 里有一个称之为
FIL_PAGE_LSN
的属性,该属性记载了最近一次修改页面时对应的 lsn 值(其实就是页面控制块中的
newest_modification
值)。如果在做了某次 checkpoint 之后有脏页被刷新到磁盘中,那么该页对应的
FIL_PAGE_LSN
代表的 lsn 值肯定大于 checkpoint_lsn
的值,凡是符合这种情况的页面就不需要重复执行
lsn 值小于 FIL_PAGE_LSN
的 redo日志 了,所以更进一步提升了奔溃恢复的速度。
好了,到这里我们 redo 日志终于讲完了,内容还是比较多的,希望大家都慢慢消化吧,我们下期见 😇😇😇