redo日志刷盘时机
- log Buffer空间不足时
log buffer 的大小是有限的,如果不停的往这个有限大小的log buffer里散入日志,很快就会被填满,如果当前写入log buffer的redo日志量已经占满了log buffer总容量的大约一半左右,就需要把这些日志刷新到磁盘上。 - 事务提交时
之所以使用redo日志主要是因为它占用的空间少,还是顺序写,在事务提交时可以不把修改过的Buffer Pool页面刷新到磁盘,但是为了保证持久性,必须要把这些页面对应的redo日志刷新到磁盘。 - 后台线程不停的刷刷刷
后台有一个线程,大约每秒都会刷新一次log buffer中的redo日志到磁盘。 - 正常关闭服务时
- 做所谓的checkpoint时
redo日志文件组
Mysql的数据目录下默认有两位名为ib_logfile0和ib_logfile1的文件,log buffer中的日志默认情况下就是刷新到这两个磁盘文件中,可以通过下边这几个参数来调节:
innodb_log_group_home_dir
:指定了redo日志所在目录;innodb_log_file_size
:指定每个redo日志文件的大小;innodb_log_files_in_group
:指定redo日志文件个数,默认值为2,最大值为100;
这些日志文件以日志文件组的形式出现,以ib_logfile[数字]的形式命名,在写入时按照数字顺序写入,如果写入到最后一个文件,那就重新转到ib_logfile0继续写。
总共的redo日志文件大小就是:innodb_log_file_size * innodb_log_files_in_group
。
redo日志文件格式
将log buffer中的redo日志刷新到磁盘的本质就是把block的镜像写入日志文件中,所以redo日志文件起始也是由若干512字节大小的block组成。
每个日志文件组每个文件大小都一样,格式也一样,都是由两部分组成:
- 前2048个字节,即前4个block用来存储一些管理信息;
- 从第2048字节往后用来存储log buffer中的block镜像。
下来介绍一下每个redo日志文件前2048个字节,也就是前4个特殊block的格式:
-
log file header:描述该redo日志文件的一些整体属性;
-
checkpint1:记录关于checkpoint的一些属性:
Log Sequeue Number
自系统开始运行就不断在修改页面,意味着会不断生成redo日志,redo日志的量在不断递增,为了记录写入的redo日志量,设计了一个称为Log Sequeue Number的全局变量,日志序列号简称为lsn。并且规定初始的lsn值为8704。
- 系统第一次启动后初始化log buffer时,bug_free就会指向第一个block的偏移量为12字节的地方,那么lsn值也会跟着增加12.
- 如果某个mtr产生的一组redo日志占用的存储空间比较小,也就是待插入的block剩余空闲空间能容纳这个mtr提交的日志时,lsn增长的量就是该mtr生成的redo日志占用的字节数。
- 如果某个mtr产生的一组redo日志占用的存储空间比较大,也就是待插入的block剩余空闲空间不足以容纳这个mtr提交的日志时,lsn增长量就是该mtr生成的redo日志占用的字节数加上额外占用的log block header和logblock trailer的字节数:
每一组由mtr生成的redo日志都有一个唯一的LSN值与其对应,LSN值越小,说明redo日志产生的越早。
flushed _to_disk_lsn
redo日志首先写到log buffer中,之后才会被刷新到磁盘上的redo日志文件,所以提出一个称为buf_next_to_write
的全局变量,标记当前logbuffer中已经有那些日志被刷新到磁盘中。
系统第一次启动时,该变量的值何处是的lsn的值是相同的,都是8704,随着系统的运行,redo日志被不断写入logbuffer 但是并不会立即刷新到磁盘,lsn的值就和flashed_to_disk_lsn的值拉开了差距。
当新的redo日志写入到log buffer时,首先lsn的值会增长,但是flushed_to_disk_lsn不变,随后随着不断有log buffer中的日志被刷新到磁盘上,flushed_to_disk_lsn的值也跟着增长,如果两者的值相同,说明log buffer中的所有redo日志都已经刷新到磁盘中了。
lsn值和redo日志文件偏移量的对应关系
初始时的LSN值是8704,对应文件偏移量2048,之后每个mtr向磁盘中写入多少字节的日志,lsn的值就增长多少。
flush链表中的LSN
在mtr结束时还有一件非常重要的事情,就是把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日志占用的磁盘空间是否可以覆盖的依据就是它对应的脏页是否已经刷新到磁盘里。
如图所示,虽然mtr1和mtr2生成的redo日志都已经被写到磁盘上,但是它们修改的脏页仍然停留再bufferpool中,所以它们生成的redo日志在磁盘上的空间是不可以被覆盖的,之后随着系统的运行,如果页a被刷新到磁盘上,那么它对应的控制块就会从兵flush链表中移除。
这样mtr1生成的redo日志就没有什么用,它们占用的磁盘空间就可以被覆盖掉,设计者提出一个全局变量checkponit_lsn
来代表当前系统中可以被覆盖的redo日志总量,这个变量初始值也是8704.
页a被刷新到磁盘,mtr1生成的redo日志就可以被覆盖了,所以我们可以进行增加checkpoint_lsn的操作,这个过程称之为checkpoint。
做一次checkpoint分为两个步骤:
- 计算当前系统中可以被覆盖的redo日志对应的lsn值最大是多少;
redo日志可以被覆盖,意味着它已经对应的脏页被刷到了磁盘,只要我们计算出当前系统中被最早修改的脏页对应的oldest_modification值,那凡是在系统lsn值小于该节点的oldest_modification值时产生的redo日志都是可以被覆盖掉的,我们就把该脏页的oldest_modification赋值给checkpoint_lsn。
- 将checkpoint_lsn和对应的redo日志文件组偏移量以及此次checkpoint的编号写到日志文件的管理信息(就是checkpoint1或checkpoint2)中。
设计者维护了一个目前系统做了多少次checkpoint的变量checkpoint_no,每做一次checkpoint,该变量的值就加1,计算一个lsn值对应的redo日志文件组偏移量是很容易的,可以计算得到该checkpoint_lsn在redo日志文件组中对应的偏移量checkpoint_offset,然后把这三个值都写到redo日志文件组的管理信息中。
每一个redo日志文件都有2048个字节的管理信息,但是上述关于checkpoint的信息只会被写到日志文件组的第一个日志文件的关系信息中。
记录完checkpoint信息后,redo日志文件组中各个lsn值得关系就像这样。
-
checkpoint_lsn之前的日志对应的脏页已经被刷新到磁盘,所以checkpoint_lsn之前的日志文件中的内容可以被覆盖。
-
checkpoint_lsn~flushed_to_disk_lsn之间的日志已经从logbuffer刷新到日志文件中,但是脏页还不能确定有没有被刷盘;
-
flushed_to_disk_lsn~lsn之间的redo日志还留在log bugger中,redo日志没有刷新到文件,并且脏页没有被刷新到磁盘。
批量从flush链表中刷出脏页
一般情况下都是后台的线程对LRU链表和flush链表进行刷脏操作,这主要因为刷脏操作比较慢,不想影响用户线程处理请求。但是如果当前系统修改页面的操作十分频繁,这样就导致写日志操作十分频繁,lsn值增长过快,如果后台的刷脏操作不能将脏页刷出,那么系统无法及时做checkpoint,可能就需要用户线程同步的从flush链表中把那些最早修改的在那个也刷新到磁盘,这样这些脏页对应的redo日志就可以做checkpoint然后覆盖了。
崩溃恢复
确定恢复的起点
checkpoint_lsn之前的redo日志对应的脏页已经被刷新到磁盘中了,所以没有必要恢复它们了,对于checkpoint_lsn之后的redo日志,它们对应的脏页可能没被刷盘,也可能被刷盘了,不能确定,所以需要从checkpoint_lsn开始读取redo日志来恢复页面。
选取最近发生的那次checkpoint的信息,衡量checkpoint发生时间早晚的信息就是所谓的checkpoint_no,只要把checkpoint1和checkpoint2这两个block中的checkpoint_no值比大小,哪个checkpoint_no更大,说明就是最近的一次checkpoint信息,这样就能拿到最近发生的checkpoint对应的checkpoint_lsn值以及它在redo日志文件组的偏移量checkpoint_offset。
确定恢复终点
普通block的log block header部分有一个称之为LOG_BLOCK_HDR_DATA_LEN
的属性,该属性记录了当前block里使用了多少字节的空间,对于被填满的block来说,该值永远为512,如果该属性值不为512,那它就是此次崩溃恢复中需要扫描的最后一个block。
怎么恢复
确定了扫描哪些redo日志进行崩溃恢复之后,接下来就是怎么进行恢复了,假设现在又redo日志文件中有5条redo日志。
按照日志中记载的内容将对应的页面恢复出来,这样没什么问题,但是为了加快这个过程:
-
使用哈希表
根据redo日志的spaceID和page number属性计算出散列值,把spaceID和page number相同的redo日志放到哈希表的同一个槽里,如果有多个spaceID和page number都相同的redo日志,那么它们之间使用链表连接起来,按照生成的先后顺序连接起来。
之后就可以遍历哈希表,因为同一个页面进行修改的redo日志都在一个槽里,所以可以一次性将一个页面修复该,避免了很多读取页面的随机IO,值得注意的是,同一个页面的redo日志是按照生成时间顺序来排序的,所以回复的时候也按照这个顺序进行恢复,如果不按照生成时间顺序进行恢复,那么可能出现错误。 -
跳过已经刷新到磁盘的页面
由于checkpoint_lsn之后的redo日志不能确定是否已经刷新到磁盘,主要是因为在最近做一次checkpoint后,可能后台线程又不断从LRU链表和flush链表中将一些脏页刷出Buffer Pool,这些在checkpoint_lsn之后的redo日志,如果它们对应脏页在崩溃时已经刷新到磁盘,那么恢复时就没有必要根据redo日志的内容修改该页面了。
那么恢复时如何知道某个redo日志对应的脏页是都在系统崩溃时已经刷盘?
每个页面都有一个称为File Header的部分,在FileHeader中有一个称为FIL_PAGE_LSN的属性,该属性记载了最近一次修改页面时对应的lsn值,如果在做了某次checkpoint后有脏页刷盘,那么该页对应的FIL_PAGE_LSN代表的lsn值肯定大于checkpoint_lsn值,凡是符合这种情况的页面就不需要重复执行lsn值小于FIL_PAGE_LSN的redo日志了。