Mysql InnoDB Redo log

2022-12-08,,,

一丶什么是redo

innodb是以也为单位来管理存储空间的,增删改查的本质都是在访问页面,在innodb真正访问页面之前,需要将其加载到内存中的buffer pool中之后才可以访问,但是在聊事务的时候,事务具备持久性,如果只在内存中修改了页面,而在事务提交后发生了系统崩溃,导致内存数据丢失,就会发生提交事务所作的更改还没来得及持久化到磁盘。

那么如何保证到提交的事务,所作更改一定持久化到磁盘了昵?

最简单粗暴的固然是,每次事务提交都将其所作更改持久化到磁盘。这种操作又存在如下问题:

刷新一个完整的页面,十分浪费IO,有时候事务所作的更改只是一个小小的字节,但由于innodb是以页为单位的对磁盘进行io操作的,这时候需要把一个完整的页刷新到磁盘,为了一个字节刷新16k的内容,是不划算的
随机io刷新缓慢,一个事务包含多个数据库操作,一个数据库操作包含对多个页面的修改,比如修改的数据不在相邻的页,存在多个不同的索引B+树需要维护,这时候需要刷新这些零散的页面,进行大量的随机IO

InnoDB是如何实现事务提交的持久化——记录下更改的内容

在事务提交的时候只记录下,事务做了什么变更到redolog,中这样即使系统崩溃了,重启之后只需要按照redo log上的内容进行恢复,重新更新数据页,那么该事务还是具备持久性的,这便是重做日志——redo log。使用redo log的好处:

redo log占用空间小:在存储事务所作变更的时候,需要存储变更数据页所在的表空间,页号,偏移量以及需要更新的值时,需要的空间很少。
redo log是顺序写入磁盘的,在执行事务的过程中,在执行每一条语句的时候,就可能产生若干条redo log,这些日志都是按照产生的顺序写入磁盘的,也就是使用顺序io

二丶redo log 日志的格式

type:redo log日志的类型
space id:表空间号
page number:页号
data:日志内容

通常一个执行语句是会修改到许多页面的,比如一个表具备多个索引,进行更新的时候需要维护聚簇索引和各种非聚簇索引,表中存在多少个索引就会可能会更新到多少个B+树,针对一颗B+树来说,极可能更新到叶子节点,也可能更新到非叶子节点,甚至还伴随着创建新页面,页面分裂,在内节点页面添加目录项等等操作。

理论上说red log只需要记录下insert 语句对页面所有的修改即可,但是插入数据到一个页面,也伴随着对这个页面的File Header,Pahe Header等等信息的修改

可见,将一条记录插入到一个页面,需要更改的地方很多,为此redo log定义了许多不同的日志格式,从物理层面来说:redo log指明了需要在哪一个表空间,的哪一个页进行什么修改内容,从逻辑层面来说:redo log在mysql崩溃恢复后,并不能直接使用这些日志记录的内容,而是需要调用一些函数,将页面进行恢复

三丶Mini-Transaction

mysql把底层页面的一次原子访问过程称为一个Mini-Transaction(MTR)(比如向B+树中插入一条记录的过程算作一个MTR,即使这个sql涉及到多个B+树)一个MTR可以包含一组redo log,在进行崩溃恢复的时候需要把这一组MTR看作是一个不可分割的整体(B+树中插入一条记录,可能涉及到叶子节点,非叶子点的改动,不能说只新增了叶子节点,但是没用更新非叶子节点,需要保证这个过程的原子性)

一个事务可以包含多个语句,一个语句可以包含多个MTR,一个MTR可以包含多个redo log日志,那么innodb是如何确认多个redo log属于一个MTR的昵

多个redo log以最后一条类型为MLOG_MULTI_REC_END类型的日志结尾,那么视为前面的redo log为同一MTR中的日志。系统在进行崩溃恢复的时候,只有解析到MLOG_MULTI_REC_END类型的日志的时候,才认为解析到了一组完整的redo log日志,才会进行恢复。

有些需要保证原子性的操作,只会产生一条redo log,比如更新Max Row ID(innodb在用户没用指定主键的时候,将此值存储在页中,没增大到256的整数倍的时候,才会更新写回磁盘),这个时候会将其的type字段的第一个比特置为1,表示这个单个redo log 便是一个原子性操作,而不是加上一个MLOG_MULTI_REC_END类型的日志。

四丶redo log日志写入过程

1.redo log block

innodb 将通过MTR生成的redo log放在大小为512字节的block中,其中存储redo log的部分只有log block body其余的两部分存储一些统计信息

log block header

log_block_hdr_no:每一个block 具备一个大于0的唯一编号,此属性便是记录编号值
log_block_hdr_data_len:记录当前lock block使用了多少字节(从12开始,因为lock block header占用了12字节),随着越来越多的日志写入block最后最大为512字节
log_block_first_rec_group:一个MTR包含多个日志,这个字段记录该block中第一个MTR生成redo log日志记录组的偏移量。
log_block_checkpoint_no:表示checkpoint的序号
log block trailer
log_block_check_sum:表示block检验和,用于正确性校验

2.redo log buffer

为了解决磁盘写入过慢的问题,innodb采用了redo log buffer,写入redo log不会直接写入磁盘,而是在服务启动的时候申请一片连续的内存空间,用作缓冲redo log的写入

innodb_log_buffer_size可以指定其大小,默认16mb

3.redo log写入log buffer

innodb保存了一个buf_free的全局变量用于记录log buffer中空闲位置的偏移量,让后续redo log的写入从buf_free的位置开始写。

不同的事务是可以并发运行的,并发的写入redo log buffer中,每当一个MTR执行完成时,伴随着该MTR生成的redo log被写入到log buffer中,多个不同事务的MTR可能时交替写如到log buffer中的

五丶redo log持久化

1.redo log buffer中的内容何时持久化到磁盘

MTR运行过程中产生的一组redo log会在MTR结束的时候被复制到log buffer中,但是何时落盘昵?

log buffer 空间不足

如果写入log buffer的redo log日志量已经占满log buffer的一半时,会进行刷盘

事务提交时

之所以使用redo log,是由于其占用内存小,可以顺序IO写回磁盘,为了保证事务的持久性,需要把修改页面对应的redo log刷新到磁盘,这样系统崩溃时也可以将已提交的事务使用redo log进行恢复

将某个脏页刷新到磁盘前

将buffer pool中的脏页刷盘的时候,会保证将其之前产生的redo log刷盘

后台线程,每秒一次的频率将redo log buffer中刷盘

正常关闭mysql服务器时

做checkpoint时

2.redo log 日志文件组

mysql的数据目录下默认有两个redo log日志文件,默认名称为ib_logfile0ib_logfile1,log buffer中的内容便是刷新到着两个文件中。可以通过以下系统变量进行设置

innodb_log_group_home_dir:指定redo log日志文件所在目录
innodb_log_file_size:指定每一个redo log日志文件大小
innodb_log_file_in_group:指定redo log日志文件的个数

日志文件不知一个,而是以一个日志文件组的形式出现的,都是ib_logfile数字格式的名称,在持久化redo log的时候,首先从ib_logfile0开始写,然后写ib_logfile1直到写到最后一个文件,这时候需要做checkpoint,后继续从ib_logfile0写,从头开始写,写到末尾就又回到开头循环写,如下图

3.redo log日志文件格式

每一个redo log文件前2048个字节(四个redo log block大小)用来存储管理信息,后续的位置存储log buffer中redo log block镜像

前2048字节分为四个block,如上图

log file header 描述该日志的一些整体属性
checkpoint1 & checkpoint2,格式相同
log_checkpoint_no:服务器执行checkpoint的编号,每执行一次checkpoint该值加1
log_checkpoint_lsn:服务器在结束checkpoint时对应的lsn值,系统崩溃时从该值开始
log_checkpoint_offset:log_checkpoint_lsn属性值,在redo日志文件组中的偏移量
log_checkpoint_log_buf_size:服务器在执行checkpoint操作时对应的log buffer大小
log_checkpoint_checksum:本block校验值,无需关心

4.LSN——log squence number记录当总共写入的redo 日志量

innodb的一个全局变量,用于记录当总共写入的redo 日志量,初始值为8704(未产生一条redo日志也是8704)。并不是记录刷到磁盘的redo log日志总量,而是写入到log buffer中的redo log日志量

每一组MTR生成的redo log都有唯一一个lsn值与之对应,其中lsn越小表示对应的redo log产生越早

5.flushed_to_disk_lsn记录刷新到磁盘中的redo日志量

redo log总是先写入到redo log buffer然后才会被刷新磁盘中的,所有需要有一个变量记录下一次从log buffer中刷盘的起始位置——buf_next_to_write

flushed_to_disk_lsn记录刷新到磁盘的redo log 日志量

当存在新的日志写入到log buffer的时候,首先lsn(log squence number记录当总共写入的redo 日志量)会增大,但是flushed_to_disk_lsn大小不变(因为没刷盘)随着log buffer中的内容刷新到磁盘,flushed_to_disk_lsn将随之增大,当flushed_to_disk_lsn = lsn的时候说明所有log buffer中的日志都刷新到了磁盘

6.buffer pool 中flush 链表中的lsn

MTR是对底层页面的一次原子访问,在访问过程中会产生一组不可以分割的redo log,在MTR结束的时候会把这一组redo log记录到log buffer中。除此之外在MTR结束的时候还需要:把MTR执行过程中修改的页面加入到Buffer pool中的flush链表中,毕竟刷新脏页到磁盘是buffer pool的任务。

当这个页面第一次被修改的时候就会将其对应的控制块插入到flush 链表头部,后续再次修改不会移动控制块,因为其已经在flush 链表中,所有说flush 链是按照页面第一次修改的时间进行排序的,在这个过程中会记录两个重要的属性到脏页的控制块中:

oldest_modification

第一次修改修改buffer pool某个缓冲页(先把页读到buffer pool的缓冲页,然后进行修改)时,记录下修改该页面MTR开始时对应的lsn(注意这个开始时)

newest_modification

每次修改页面,都会记录下修改该页面MTR结束时的lsn,该属性记录最近一次修改后对应的lsn

例如第一个MTR1修改了页A,在oldest_modification记录下开始时的lsn为8716,newest_modification记录下MTR1结束时的lsn

MTR2后续修改了页B和页C,页B,页C最开始不在flush链表中,背修改后加入到链表的头部,也就是说,越靠近头部,修改的时间就越晚,如下:

如果此时在flush链表中的页被修改,只需要更新newest_modification即可。flush链表按照第一次被更新时候的lsn排序,也就是按照oldest_modification进行排序,多次修改位于flush链表中的页只会更新其newest_modification

7.checkpoint

由于redo log日志文件文件是有限的,所有需要循环使用redolog 日志文件组中的文件。redo log存在的目的是系统崩溃后恢复脏页,如果对应的脏页已经刷新到磁盘中,那么即使系统崩溃,重启之后页不需要使用redo log恢复该页面看,对应的redo log日志也没存在的必要了,占用的磁盘空间可以进行重复使用,被其他redo log日志覆盖。那么如何判断哪些redo 日志占用的磁盘空间可以覆盖昵,其对应的脏页已经刷新到磁盘了昵?

innodb使用checkpoint_lsn记录可以被覆盖的redo log日志总量

假如页A已经被刷新到磁盘了,那么页A对应的控制块会从flush链表中移除,MTR1生成的日志可以被覆盖了,就进行一个增加checkpoint_lsn的操作,这个过程称作执行一次checkpoint(刷新脏页到磁盘和执行一次checkpoint是两回事,通常是不同线程上执行的,并不意味着每次刷新脏页的时候都会执行一次checkpoint)

执行一次checkpoint的操作可以分为两个步骤

    计算当前系统中可以覆盖的redo日志对应的lsn最大是多少

    比如页A已经被刷盘了,此时flush链表的尾部便是页C,其oldest_modification=8916那么说明redo log日志对应lsn值小于8916的均可被覆盖,checkpoint_lsn的值会被设置为8916

    将checkpoint_lsn和对应的日志文件组偏移量和这次checkpoint的编号写入到日志文件的管理信息中(checkpoint1,checkpoint2)

    innodb使用checkpoint_no记录当前系统执行了多少次checkpoint,根据lsn值计算除redo log日志的偏移量(lsn初始值为8704,redo日志文件组偏移量为2048)计算得到checkpoint_offset,将checkpoint_no,checkpoint_lsn,checkpoint_offeset写回到redo log日志文件组管理信息中,关于checkpoint的信息,当checkpoint_no为偶数的时候会写回到checkpoint1,反之写入到checkpoint2中。

8.控制事务提交时刷新redo log日志的选项——innodb_flush_log_at_trx_commit

如果每次事务提交时都要求将redolog刷新到磁盘,那么带来的IO代价必然影响到引擎执行效率,innodb具备innodb_flush_log_at_trx_commit配置项,来进行控制。其选项值的不同代表着不同的策略:

    0:表示事务提交不立即向磁盘同步redo log,而是交由后台线程处理。这样的好处时加快处理请求的速度,但是如果服务崩溃,后台线程也没来得及刷新redo log,这时候会丢失事务对页面的处理
    1:表示每次事务提交都需要将redo log同步到磁盘,可以保证事务的持久性,这也是innodb_flush_log_at_trx_commit的默认值
    2:表示事务提交时,将redo log写到操作系统的缓冲区中,但是并不需要真正持久化到磁盘,这样事务的持久性在操作系统没有崩溃的时候还是可以保证,但是如果操作系统也崩溃那么还是无法保证持久性

六丶redo log用于崩溃恢复

1.确定恢复的起点

如果redo log对应的lsn小于checkpoint的话,意味这部分日志对应的脏页已经刷新到了磁盘中,是不需要进行恢复的。但是大于checkpoint的redo log,也许时需要恢复的,也许不需要,因为刷新脏页到磁盘是异步进行的,可能刷新脏页到磁盘但是没来得及修改checkpoint。这时候需要从对应lsn的值为checkpoint_lsn的redo log开始恢复

redo log日志文件组有checkpoint1和checkpoint2记录checkpoint_lsn的值,我们需要选取最近发生的checkpoint信息,也就是将二者中的checkpoint_no拿出来比较比较,谁大说明谁存储了最近一次checkpoint信息,从而拿到最近发生checkpoint的checkpoint_lsn以及其对应的在redo 日志文件组中偏移量checkpoint_offset

2.确定恢复的终点

写入redo log到redo日志文件中redo log block中时,是顺序写入的,先写满一个block再写下一个block,每一个block的log block header部分有一个log_block_hdr_data_len来记录当前lock block使用了多少字节(从12开始,因为lock block header占用了12字节),随着越来越多的日志写入block最后最大为512字节`,所有如果此值小于512那么说明,当前这个block就是崩溃恢复需要扫描的最后一个block。

在mysql进行崩溃恢复的时候,只需要从checkpoint_lsn在日志文件组中对应的偏移量开始,扫描到第一个log_block_hdr_data_len值不为512的block为止

3.如何进行崩溃恢复

现在确定了需要恢复的redo log,那么如何进行恢复昵,每一个redo log格式如下

3.1化随机io为顺序io

其space id记录了表空间号,page number记录了页号,也许这一堆redo log整体上表空间号,页号是不具备顺序的,如果直接遍历每一个redo log然后对表空间中的页进行恢复,是会带来很多随机io的,所以innodb使用hash表进行优化,将相同表空间号和页号作为key,这样相同表空间和页的日志就会在同一个hash桶中形成链表中,然后遍历每一个hash表的操作,一次性将一个页进行恢复,化随机io为顺序io

3.2跳过不需要刷新页

首先小于最近一次checkpoint_lsn的redo log肯定是不需要进行恢复,但是大于的也不一定需要恢复,因为可能在做崩溃前的checkpoint的时候,后台线程也许将LRU链表和flush链表中的一些脏页刷新到磁盘了,那么恢复的时候这些脏页也不需要进行恢复。那么怎么判断这些不需要恢复的脏页昵?——每一个页面具备file header,其中有一个属性为file_page_lsn的属性,记录了最近一次修改页面时对应的lsn值(即脏页在buffer pool控制块中的newest_modification)如果执行某次checkpoint发现页中的lsn大于最近一次checkpoint的checkpoint_lsn的时候,那么说明此页不需要进行更新。

Mysql InnoDB Redo log的相关教程结束。

《Mysql InnoDB Redo log.doc》

下载本文的Word格式文档,以方便收藏与打印。