MySQL是怎样运行的:(21)redo日志(下)
Session 21 redo日志(下)
redo日志缓冲区的刷盘时机:
log buffer空间不足时。如果当前写入log buffer的redo日志量已经占满了log buffer总容量的大约一半左右,就需要把这些日志刷新到磁盘上- 事务提交时。在事务提交时可以不把修改过的
Buffer Pool页面刷新到磁盘,但是为了保证持久性,必须要把修改这些页面对应的redo日志刷新到磁盘(可通过系统变量innodb_flush_log_at_trx_commit控制) - 后台有一个线程,大约每秒都会刷新一次
log buffer中的redo日志到磁盘 - 正常关闭服务器时
- 做所谓的
checkpoint时(稍后会仔细介绍)
磁盘上的redo日志文件不只一个,而是以一个日志文件组的形式出现的。这些文件以ib_logfile[数字](数字可以是0、1、2…)的形式进行命名。(详见文章)
redo日志文件格式
redo日志文件其实也是由若干个512字节大小的block组成。redo日志文件组中的每个文件大小都一样,格式也一样,都是由两部分组成:
- 前2048个字节,也就是前4个block是用来存储一些管理信息的。
- 从第2048字节往后是用来存储
log buffer中的block镜像的。

日志序列号(LSN)
为记录已经写入的redo日志量,设计了一个称之为Log Sequeue Number的全局变量,翻译过来就是:日志序列号,简称lsn。规定初始的lsn值为8704(人为规定的)。
我们知道,在向log buffer中写入redo日志时不是一条一条写入的,而是以一个mtr生成的一组redo日志为单位进行写入的。而在统计lsn的增长量时,是按照实际【写入的日志量(mtr大小)】加上占用的log block header和log block trailer来计算的。

(上图中,mtr1大小为200,8916=8704+12+200)
可以看出来,每一组由mtr生成的redo日志都有一个唯一的LSN值与其对应,LSN值越小,说明redo日志产生的越早。
flushed_to_disk_lsn
redo日志是首先写到log buffer中,之后才会被刷新到磁盘上的redo日志文件。所以设计InnoDB的大佬提出了一个称之为buf_next_to_write的全局变量,标记当前log buffer中已经有哪些日志被刷新到磁盘中了。

当有新的redo日志写入到log buffer时,首先lsn的值会增长,但flushed_to_disk_lsn不变,随后随着不断有log buffer中的日志被刷新到磁盘上,flushed_to_disk_lsn的值也跟着增长。如果两者的值相同时,说明log buffer中的所有redo日志都已经刷新到磁盘中了。
flush链表中的LSN
在mtr结束时,会把对应的一组redo日志写入到log buffer中。除此之外,在mtr结束时还有一件非常重要的事情要做,就是把在mtr执行过程中可能修改过的页面加入到Buffer Pool的flush链表。如果被修改的页面已经在flush链表中了,就不再次插入了。也就是说flush链表中的脏页是按照页面的第一次修改时间从大到小进行排序的。在这个过程中会在缓存页对应的控制块中记录两个关于页面何时修改的属性:
oldest_modification:该页面对应的mtr的lsn值。(flush链表可看作按此值从头到尾排序)。newest_modification:最后一个修改该页面的mtr的lsn值。
例子。(为了方便,把oldest_modification缩写成了o_m,把newest_modification缩写成了n_m)

checkpoint
有一个很不幸的事实就是我们的redo日志文件组容量是有限的,我们不得不选择循环使用redo日志文件组中的文件,但是这会造成最后写的redo日志与最开始写的redo日志追尾,这时应该想到:redo日志只是为了系统奔溃后恢复脏页用的,如果对应的脏页已经刷新到了磁盘,那么该redo日志也就没有存在的必要了,那么它占用的磁盘空间就可以被后续的redo日志所重用。也就是说:判断某些redo日志占用的磁盘空间是否可以覆盖的依据,就是它对应的脏页是否已经刷新到磁盘里。
继续刚刚的例子,如果页a被刷新到了磁盘,那么它对应的控制块就会从flush链表中移除:

这样mtr_1生成的redo日志就没有用了,它们占用的磁盘空间就可以被覆盖掉了。用一个全局变量checkpoint_lsn来代表当前系统中可以被覆盖的redo日志总量是多少,这个变量初始值也是8704。
redo日志可以被覆盖,意味着它对应的脏页被刷到了磁盘,只要我们计算出当前系统中被最早修改的脏页对应的oldest_modification值,那凡是在系统lsn值小于该节点的oldest_modification值时产生的redo日志都是可以被覆盖掉的,我们就把该脏页的oldest_modification赋值给checkpoint_lsn。我们把这个过程称之为做一次checkpoint。
比方说当前系统中页a已经被刷新到磁盘,那么flush链表的尾节点就是页c,该节点就是当前系统中最早修改的脏页了,它的oldest_modification值为8916,我们就把8916赋值给checkpoint_lsn

崩溃恢复
在重启时根据redo日志中的记录就可以将页面恢复到系统奔溃前的状态。我们接下来大致看一下恢复过程是什么样。
起点:显然我们需要从checkpoint_lsn开始读取redo日志来恢复页面。
终点:第一个未被填满的block(即LOG_BLOCK_HDR_DATA_LEN属性不为512B)
加快恢复(详见文章)
- 使用哈希表把相邻的页连接在一起
- 跳过已经刷新到磁盘的页面(已落盘但还没来得及checkpoint的页)