Mysql InnoDB Redo log
参考书籍《mysql是怎样运行的》
一丶什么是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_logfile0
和ib_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的时候,那么说明此页不需要进行更新。