InnoDB使用Buffer Pool来加速数据读写,提升性能的同时也带来了一些问题,为了避免页面频繁刷盘和磁盘随机写,InnoDB引入了WAL机制,先顺序写少量的redo log,再由后台线程去异步刷脏页,尽可能提升SQL的执行效率。
写redo log的好处是:相较于一个完整的页,redo log占用的空间极小,而且它是顺序写的,比随机写的效率更高。
根据不同的修改场景,InnoDB设计了几十种不同类型的redo log。这里面包含极其简单的物理日志,只是单纯记录了要把哪个页面的哪些数据做修改;还包含复杂的逻辑日志,它只保留了必要数据,需要调用一些提前准备好的系统函数才能恢复页面。
redo log是以组的形式来写入的,一组redo log是不可分割的,要么不恢复,要么全部恢复。InnoDB把这种对底层页面的一次原子访问称作一个Mini Transaction,缩写MTR,一个MTR就对应一组redo log。
生成的redo log怎么存储呢?
为了更好的管理,InnoDB设计一个叫redo log block
的结构,它和页很像。一个redo log block占用固定的512字节,结构如下:
属性 | 长度 | 说明 |
---|---|---|
log block header | 12字节 | block头信息 |
log block body | 496字节 | 存放redo log |
log block trailer | 4字节 | 存放block checkSum |
中间496字节的body部分用来存储实际的redo log,重点关注header部分,结构如下:
属性 | 长度 | 说明 |
---|---|---|
LOG_BLOCK_HDR_NO | 4字节 | block唯一编号 |
LOG_BLOCK_HDR_DATA_LEN | 2字节 | block已经使用了多少字节,512代表全部写满 |
LOG_BLOCK_FIRST_REC_GROUP | 2字节 | MTR第一条日志的起始地址 |
LOG_BLOCK_CHECKPOINT_NO | 4字节 | checkpoint序号 |
磁盘IO一直是数据库的性能杀手,事务执行期间会生成大量的redo log,如果每次都写磁盘,效率是很差的。MySQL会在启动时向操作系统申请一块连续的内存空间,然后将其划分成若干个redo log block,这就是redo log buffer。
MySQL通过启动项innodb_log_buffer_size
来控制redo log buffer的大小,默认是16MB。
redo log buffer也是顺序写入的,从前往后写,InnoDB提供了一个全局变量buf_free
,buf_free前面的是已经使用的空间,后面的是空闲空间,通过buf_free就知道每次该往哪个block的哪个位置写了。
redo log是以组的方式写入的,它们首先会被暂存起来,等MTR结束时再统一复制到redo log buffer里。
redo log buffer空间很有限,一直往里写也不是个事儿,InnoDB会在下述场景将redo log buffer刷新到磁盘:
redo log buffer空间有限,部分场景下InnoDB会将log buffer里的redo log刷新到磁盘,对应的磁盘文件就是redo log file。
redo log file由一组文件组成,默认情况下会有ib_logfile0
和ib_logfile1
两个日志文件,可通过启动项innodb_log_group_home_dir
指定日志文件的路径,innodb_log_file_size
指定单个日志文件的大小,innodb_log_files_in_group
指定日志文件的个数。
redo log file是循环写的,从下标为0的文件开始写,写满了自动切换到下一个,都写满了又会从下标为0的文件开始写。
循环写会存在“追尾”的问题,InnoDB引入了checkpoint操作,必须确保redo log可以被覆盖。怎么判断呢?只要redo log对应的脏页已经刷新到磁盘,那么redo log就没用了,可以被覆盖。
redo log file的格式很简单,就是log buffer里redo log block的镜像,唯一的区别是redo log file使用前4个block来存储log file的头信息和checkpoint相关的信息。也就是说,redo log file是从第2048个字节开始写入redo log block的。
第一个block存储log file头信息,格式如下:
属性 | 长度 | 说明 |
---|---|---|
LOG_HEADER_FORMAT | 4字节 | redo log版本 |
LOG_HEADER_PAD1 | 4字节 | 填充字节 |
LOG_HEADER_START_LSN | 8字节 | 2048字节处对应的lsn值 |
LOG_HEADER_CREATOR | 32字节 | log file创建者 |
LOG_BLOCK_CHECKSUM | 4字节 | 校验和 |
第三个block目前没使用,第二和四个block分别存储checkpoint1和checkpoint2,格式是一样的:
属性 | 长度 | 说明 |
---|---|---|
LOG_CHECKPOINT_NO | 8字节 | checkpoint编号 |
LOG_CHECKPOINT_LSN | 8字节 | 最后一次checkpoint对应的lsn值 |
LOG_CHECKPOINT_OFFSET | 8字节 | 最后一次checkpoint对应的lsn值在文件里的偏移量 |
LOG_CHECKPOINT_LOG_BUF_SIZE | 8字节 | 执行checkpoint时log buffer的大小 |
LOG_BLOCK_CHECKSUM | 4字节 | 校验和 |
checkpoint操作时,需要把此次checkpoint相关的信息写入到第二和第四个block里,当checkpoint_no是偶数时写入到checkpoint1,奇数写入到checkpoint2。
InnoDB有一个全局变量log sequence number
,缩写lsn,它代表redo log写入的日志总量(因为redo log是写入到redo log block里面的,所以lsn的大小还包含了block header和block trailer的大小)。它的初始值是8704,MTR结束时会把redo log复制到redo log buffer里,同时lsn会累加上对应的redo log占用的空间大小。lsn是不断累加的,不可能减少,这意味着lsn越小对应的redo log生成的越早!
redo log buffer会在适当的时机刷盘,InnoDB有一个全局变量buf_next_to_write
代表log buffer中哪些log已经写入磁盘,它到buf_free
的部分代表日志已经写入log buffer,但是还没写入磁盘,两者相等代表log buffer里的所有日志都写入到磁盘了。
InnoDB还有一个全局变量flushed_to_disk_lsn
代表系统写入磁盘的redo log日志总量,它和lsn的区别是:lsn代表系统生成的redo log日志总量,这包含写入了redo log buffer,但是还没写入磁盘的redo log,如果flushed_to_disk_lsn
和lsn相等,代表所有redo log都写入到磁盘了。
redo log首先被写到redo log buffer,通过变量buf_free
InnoDB就知道该往log buffer的哪个位置写了。当redo log buffer要刷盘时,那么首先面临的问题就是:要写到哪个redo log file的哪个位置?
这个问题很好解决,由于redo log file文件大小是固定的,且是顺序写的,全局变量flushed_to_disk_lsn
代表redo log写入磁盘的日志总量,根据该lsn值很容易计算它对应到哪个redo log file的偏移量。
MTR结束时,除了把redo log复制到redo log buffer里,还需要把Buffer Pool里被修改的脏页加入到flush链表,后台线程会异步的将这些脏页同步到磁盘,只要脏页同步到磁盘,那么它对应的redo log就没用了。
把脏页加入到flush链表,其实就是把脏页对应的控制块加入到flush链表的表头,同时控制块会有两个属性:
oldest_modification
会在索引页第一次被修改时写入对应的LSN值,当脏页被再次修改时,不会再移动它的位置了,仅仅是将新的LSN值写入newest_modification
属性。
如此一来,flush链表的链尾控制块里的oldest_modification,就是当前所有脏页的最小LSN值。也就是说,凡是小于该LSN的redo log,都是可以被覆盖的,这方便了后续的checkpoint操作。
命令SHOW ENGINE INNODB STATUS
可以查看InnoDB引擎的状态信息,这里面就包含各种LSN的值。
Log sequence number 7853861756693
Log flushed up to 7853861756453
Pages flushed up to 7853579810002
Last checkpoint at 7853579810002
lsn
。flushed_to_disk_lsn
。oldest_modification
。checkpoint_lsn
值。redo log日志文件组的容量大小是有限的,而redo log在系统运行过程中是不断产生的,redo log file采用循环写的方式,终有一天会发生“追尾”。怎么办呢?这就是checkpoint操作要干的活儿。
再次回顾一下,redo log的作用是什么?它是为了防止Buffer Pool里的脏页还没来得及刷盘时,系统崩溃后做数据恢复用的。也就是说,只要Buffer Pool里的脏页刷盘了,那么这些已经刷盘的脏页对应的redo log就一点用都没有了,这些redo log是可以被覆盖的。
如何判断哪些redo log可以被覆盖?
Buffer Pool里有一条flush链表,它的链尾元素是脏页对应的控制块,代表当前还未被刷盘且最早被修改的脏页。控制块里有oldest_modification
属性代表脏页最早修改时的LSN值,redo log file中小于该LSN的redo log都是可以被覆盖的。
InnoDB有一个全局变量checkpoint_lsn
,它代表当前可以被覆盖的redo log日志总量是多少。所谓的checkpoint其实非常简单,分为两步:
oldest_modification
属性值赋值给checkpoint_lsn
。checkpoint_lsn
、checkpoint_no
、checkpoint_offset
等信息写入到redo log file的checkpoint1或checkpoint2中。checkpoint需要将checkpoint相关信息写入到redo log file文件,因此它是有代价的。
事务提交时,redo log需要写入到磁盘,尽管是顺序写,但毕竟是磁盘啊,这个代价还是很大的。所以InnoDB提供了innodb_flush_log_at_trx_commit
选项,可以让你配置redo log的刷盘策略:
综上所述,只有配置1才是符合事务的持久性,0和2可以换来事务性能上的提升,但前提都是以牺牲数据安全为代价的,除非你对数据安全没有强要求,否则都不建议修改该配置。
只要MySQL进程正常运行,redo log完全就是个累赘,不仅没有任何作用,还会拖累数据库的性能。但是只要发生故障崩溃了,那redo log的重要性就体现出来了,MySQL重启时会利用redo log进行数据恢复,将崩溃前Buffer Pool里被修改的还没来得及刷盘的脏页给恢复回去。
恢复的第一件事,就是确定恢复的起点。redo log file文件可能会很大且很多,该从哪个文件的哪个位置开始恢复呢?checkpoint_lsn
代表可以被覆盖的redo log日志总量,redo log可被覆盖意味着对应的脏页已经刷新到磁盘了,也就是说,小于checkpoint_lsn
的redo log是不用恢复的,checkpoint_lsn
就是InnoDB恢复的起点。
上哪儿去找checkpoint_lsn
?checkpoint操作时,InnoDB会将checkpoint相关信息写入到redo log file的第二或第四个block里,也就是checkpoint1或checkpoint2。只需要将checkpoint1和checkpoint2读取出来,比较一下checkpoint_no
的大小就知道最近一次checkpoint操作时对应的checkpoint_lsn
了,以及它在redo log file的偏移量checkpoint_offset
。
恢复的起点确定了,接下来就是往后顺序读取redo log并解析,然后调用系统函数进行页面恢复。由于redo log file是顺序写的,该如何确定恢复的终点呢?也很简单,redo log block的header部分有一个属性LOG_BLOCK_HDR_DATA_LEN
,它代表当前block被使用的大小。恢复时,只要读取到第一个LOG_BLOCK_HDR_DATA_LEN
值小于512的block,就意味着是终点了。
事实上,MySQL针对redo log的恢复还做了一些优化来加速恢复过程:
FIL_PAGE_LSN
属性,记录了最近一次修改当前页的LSN值,如果redo log的LSN值小于它,那么该redo log就可以不用执行了。事务执行期间,InnoDB先修改内存里的缓存页数据,然后生成redo log记录下对页做了哪些修改,累加lsn值,并将脏页加入到flush链表,同时写入oldest_modification
记录下当时的lsn值,最后将redo log复制到redo log buffer中。事务提交时,redo log buffer刷盘,redo log会被写入到redo log file中。只要redo log刷盘成功,事务就算提交完成了,尽管脏页还没有刷盘。脏页会由后台线程异步刷盘,哪怕系统崩溃也没关系,MySQL重启时会根据redo log恢复数据页。
redo log file是循环写的,会存在追尾的情况。所以InnoDB会有checkpoint操作,后台线程异步的将脏页刷盘,只要脏页同步到磁盘,它对应的redo log就可以被覆盖了。由于lsn是redo log的日志总量,不断递增不会减小,所以所谓的checkpoint操作其实就是将flush链表里最早的lsn赋值给checkpoint_lsn,并将此次checkpoint相关的数据写入到redo log file里而已。