背景
本文涉及的内容较为底层,做了解即可,是以前学习《高性能Mysql》和《mysql是怎样运行的》的笔记整理所得。
redolog(后续使用redo日志 表示)的核心作用是保证数据库的持久性。
在mysql系列5---Innodb的缓存中介绍过:数据和索引保存在磁盘上,为提高效率读写时需从磁盘将数据加载到内存(Buffer Pool)中,并基于内存进行读写。内存中的数据不稳定,当系统断电或者崩溃时数据会丢失,因此所有修改最终都要刷入磁盘。
数据库事务具有持久性:事务提交成功后,无论数据库环境如何(数据库或者操作系统崩溃),已提交事务涉及的修改都会被保存下来。
最简单的实现方法是每次事务提交都将Buffer Pool中的脏页刷入磁盘,然后返回事务提交成功。这意味着每次事务提交时都进行刷盘,会带来如下三个问题:
1\] 刷盘相比内存操作的速度慢太多,会严重影响mysql整体性能; \[2\] SQL可能只修改了一个字节,然而需要以页为单位进行刷盘; \[3\] SQL可能影响多行记录,这些行可以位于不同的页中,涉及随机IO,效率较低。 mysql引入redo日志为其提供了一个解决方案。redo日志通过特殊的格式设计从而占据较小的空间,且redo日志的顺序写入相对随机IO效率较高。因此,相对于直接将更新刷入磁盘,将修改记录(redo日志)刷入磁盘可以在保证数据库持久性的前提下最大可能降低对数据库性能的影响。 ## 1.redo文件介绍 本章先从整体上对redo文件的格式进行认识,为后续章节作准备。内容包括对redo日志文件组、文件格式、块和每条redo日志格式的介绍,泛泛了解即可。  整体结构图如上所示,以下分别进行介绍。 ### 1.1 redo文件组 redo文件以文件组的形式存在,文件组首尾串联。先写向第一个文件,写满后再转向下一个,循环复用,如下图所示:  redo日志文件组的实现在mysql8和mysql5中有所区别。 #### mysql5 (1) 存放路径 由innodb_log_group_home_dir变量确定日志的存储路径,默认与mysql的数据目录同一路径; ```mysql mysql> SHOW VARIABLES LIKE 'innodb_log_group_home_dir'; +---------------------------+-------+ | Variable_name | Value | +---------------------------+-------+ | innodb_log_group_home_dir | ./ | +---------------------------+-------+ ``` (2) 文件个数 由innodb_log_files_in_group变量确定文件组中文件的个数,默认为2; ```mysql mysql> SHOW VARIABLES LIKE 'innodb_log_files_in_group'; +---------------------------+-------+ | Variable_name | Value | +---------------------------+-------+ | innodb_log_files_in_group | 2 | +---------------------------+-------+ ``` (3) 文件大小 由innodb_log_file_size变量确定每个文件的大小,默认为50331648比特,即48M. ```mysql mysql> SHOW VARIABLES LIKE 'innodb_log_file_size'; +----------------------+----------+ | Variable_name | Value | +----------------------+----------+ | innodb_log_file_size | 50331648 | +----------------------+----------+ ``` 进入对应目录查询实际文件: ```shell [root@124 mysql]# cd /var/lib/mysql [root@124 mysql]# ls -al | grep ib_logfile -rw-r-----. 1 mysql mysql 50331648 12月 15 09:46 ib_logfile0 -rw-r-----. 1 mysql mysql 50331648 10月 29 00:00 ib_logfile1 ``` #### mysql8 (1) 存放路径: 存放路径位于datadir的#innodb_redo文件夹相下,即默认为`/var/lib/mysql/#innodb_redo`. (2) 文件个数和文件大小 由变量innodb_redo_log_capacity控制文件组的整体大小,默认为100M;文件个数固定为32个,因此每个文件大小为3276800比特. ```shell [root@localhost #innodb_redo]# cd /var/lib/mysql/#innodb_redo [root@localhost #innodb_redo]# ll -t 总用量 102400 -rw-r-----. 1 mysql mysql 3276800 12月 15 10:11 #ib_redo13030 -rw-r-----. 1 mysql mysql 3276800 12月 12 16:29 #ib_redo13029 -rw-r-----. 1 mysql mysql 3276800 12月 11 07:31 #ib_redo13028 -rw-r-----. 1 mysql mysql 3276800 12月 5 17:19 #ib_redo13059_tmp -rw-r-----. 1 mysql mysql 3276800 12月 5 17:19 #ib_redo13058_tmp -rw-r-----. 1 mysql mysql 3276800 12月 5 17:19 #ib_redo13057_tmp ... -rw-r-----. 1 mysql mysql 3276800 12月 5 17:19 #ib_redo13033_tmp -rw-r-----. 1 mysql mysql 3276800 12月 5 17:19 #ib_redo13032_tmp -rw-r-----. 1 mysql mysql 3276800 12月 5 17:19 #ib_redo13031_tmp ``` ### 1.2 redo文件和redo块 对文件组有概念后,再看一下redo文件。 每个redo文件由两部分组成: 文件头+数据部分;  文件头占据2048个字节,存储当前redo文件的管理信息;数据部分存储redo日志。 redo文件也以页为单位进行管理,不过一个redo页占据512K, 后面用log block表示(与前面介绍的数据页进行区分)。其中文件头占据前4个log block, 数据部分由多个log block(redo页)组成,数据部分用于存放实际的redo数据。 再看一下log block的结构,由头部、主体和尾部组成, 如下图所示:  header和tailer存储log block的管理信息和校验信息,body中存放redo数据。 其中,header字段中有两个属性比较重要: \[1\] log_block_hdr_no: 表示log block的唯一ID; \[2\] **log_block_hdr_data_len** : 表示当前log block已使用了多少字节;初始值为12(header长度),当496个body全部写完后,log_block_hdr_data_len为512.**\[后面用到\]** 还有个地方需要注意一下(可先跳过,看第5章时再折回): 尽管redo日志文件组的每个文件都有一个文件头且占据了2048个字节的管理信息;仅第一个文件管理信息中使用两个log block(checkpoint1和chekpoint2)记录了checkpoint信息,包括checkpoint_no(checkpoint编号)和checkpoint_lsn(checkpoint时的lsn); 每次checkpoint时,checkpoint_no会递增,根据奇偶性依次写入checkpoint1和checkpoint2,以防止某次写错导致数据无法恢复。**\[后面用到\]** ### 1.3 redo日志格式 每条redo日志包含的信息有"**在哪个表空间的哪个页上做什么** ",如: "将第10号表空间的100号页面的偏移量为1000处的值更新为10000"。 redo日志的格式设计如下:  \[1\] type表示redo日志的类型,决定了data的数据组成和解析过程,type字段主要是为了节省空间; \[2\] spaceId和pageId表示表空间ID和数据页ID,用于定位数据变更发生的的位置; \[3\] data记录改动的具体内容。 ## 2.mini-transaction单元 上述redo日志格式中包含了表空间、页序号、修改数据等信息,记录了一个修改操作的所有信息。mini-transaction(以下用mtr表示)是将一组相关联的redo日志组合成一个组,mysql将一个mtr设计为一个原子操作。mtr的原子性设计为redo日志的持久化功能提供了基础,以下结合案例进行说明。 假设B+树索引的页A、B、C位于同一个表空间(tablespace001),页B比较空闲(只有一条记录),当插入一行主键为50的记录时,索引树的变化如下图所示:  此时涉及一个改动,在tablespace001的页B中添加一条记录50;对应的redo日志只有一条记录,近似表示为: ```shell 在tablespace001表空间的页B中添加一条记录50; ``` 然而,如果页b已存满记录,50通过计算需要存储在页b中,此时需要经历页裂过程:  将页B裂解为页B和页D,涉及的步骤有: \[1\] 创建一个页D; \[2\] 将页B的51和90记录前移到页D; \[3\] 在页A中新增一个目录记录项,指向页D; \[4\] 将记录50添加到页B中. 此时,对应的redo日志有多条记录, 近似表示为: ```shell 在tablespace001表空间创建一个页D; 在tablespace001表空间的页A中添加一个目录项指向页D; 向tablespace001表空间的页D中添加记录51; 向tablespace001表空间的页D中添加记录90; 删除tablespace001表空间的页B的51记录; 删除tablespace001表空间的页B的90记录; 修改tablespace001表空间中页B的后驱节点为页D; 修改tablespace001表空间中页D的前驱节点为页B; 修改tablespace001表空间中页C的前驱节点为页D; 修改tablespace001表空间中页D的后驱节点为页C; 在tablespace001表空间的页B中添加一条记录50; ... # 此外,此时页A页的空间也不足够添加新的目录项记录,则对页A需要进行裂解,页A如果还有上级... ``` 插入一条记录对一个索引树的修改对应多条记录,这些redo日志需要具备原子性以保证B+树的正确性,如不能仅仅完成了页的裂解而没有拷贝数据,不能仅创建了叶子节点而未在父节点新增目录项记录等。 上述这些redo日志被标记为一个mtr,一个mtr内的redo日志必须完整。mtr的设计为日志的崩溃后恢复提供了较好的设计基础,将在第5章中介绍。 上述一条INSERT语句仅仅是针对一个索引树,一般一个表有多个索引,因此一条INSERT语句一般对应多个mtr, 如自增主键也是一个mtr. 所以可用下图表示SQL与mtr与redo日志的关系:  ## 3.log buffer 与Buffer Pool类似,mysql引入log buffer作为redo日志的内存缓存以加快IO速度、提高系统性能。mysql启动时在内存中划出一块区域用于缓存redo日志,这部分区域叫做log buffer. log buffer也是以log block为单元进行管理, 即redo日志先写入到log buffer再刷新到磁盘。 log buffer内存大小由innodb_log_buffer_size变量确定,默认为16777216比特,即16M. ```mysql mysql> SHOW VARIABLES LIKE 'innodb_log_buffer_size'; +------------------------+----------+ | Variable_name | Value | +------------------------+----------+ | innodb_log_buffer_size | 16777216 | +------------------------+----------+ ``` 对于log buffer的理解可以从两个角度进行,数据写入到log buffer以及log buffer刷盘到redo日志文件; 将redo日志写到缓冲区是以mtr为单元进行,而将缓存写到磁盘是以redo页(log block)为单位进行。 ### 3.1 写入log buffer mysql将redo日志写入log buffer是以mtr为单位进行,以下结合案例介绍写入过程。 假设有两个事物A和B,事务A和B分别包括两个mtr, 按照时间的先后的执行顺序依次为mtr-a-1, mtr-b-1, mtr-a-2,mtr-a-2:  将mtr写入log buffer的流程图如下所示:  说明:每个mtr都有一个唯一标识lsn,即每个mtr内的所有redo日志具有相同的lsn。 ### 3.2 log buffer写入磁盘 考虑到机器断电异常会导致内存数据丢失,而redo日志本身就是为了保证数据库事务的持久性而引入,因此何时对log buffer进行刷盘很重要。涉及到什么时候将log buffer刷入到磁盘。  log buffer的刷盘时机有以下几种: \[1\] log buffer空间不足时(占据了总容量的一半),需要刷盘以预留出足够的内存空间缓存新的redo日志; \[2\] 事务提交时刷盘; \[3\] 后台线程每隔1秒触发一次刷盘; \[4\] 正常关闭服务器时,为了保证redo日志持久化,需要刷盘; \[5\] checkpoint时,将在第四章中介绍; 其中,当事务提交时立即刷盘log buffer,刷盘完成后上报事务提交成功,保证了redo日志的持久性。 根据业务对持久性的要求不同,mysql提供了一个调优参数innodb_flush_log_at_trx_commit. ```mysql mysql> SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit'; +--------------------------------+-------+ | Variable_name | Value | +--------------------------------+-------+ | innodb_flush_log_at_trx_commit | 1 | +--------------------------------+-------+ ``` \[1\] innodb_flush_log_at_trx_commit默认为1,表示事务提交时立即将事务涉及的log buffer刷盘,确保了事务的持久性; \[2\] 0表示不主动刷盘,由后台线程异步刷盘(1s一次),如果数据库宕机会导致部分数据丢失; \[3\] 2表示将输入拷贝到操作系统缓冲区,由操作系统刷盘; 数据库宕机不影响数据的持久性,但操作系统宕机会导致数据丢失。 本文后续以默认的刷盘机制(innodb_flush_log_at_trx_commit=1)进行介绍。 ### 3.3 log buffer清理 在理解log buffer清理问题前,需要先理解两个变量: lsn和flushed_to_disk_lsn。 lsn是个全局变量,表示日志序列号(Log Sequeue Number),用于记录生成的redo日志量,从8704开始不断递增。由于redo日志是以mtr为单位写入log buffer的,所以整个mtr内的redo日志具有相同的lsn;不同的mtr具备不同的lsn, 且lsn越小对表示redo日志生成的越早。  如上图所示,mtr1对应的lsn是8704, mtr2对应的日志是8804(9704+100),下一个mtr的lsn对应8954(8804+150),后续lsn取决于上一个mtr的长度。 flushed_to_disk_lsn表示已被刷新到磁盘的lsn,即lsn小于flushed_to_disk_lsn的redo日志已经被刷入磁盘,这部分log buffer内存可被清理:  图中最左侧的log buffer区域可被清理和重复利用。当flushed_to_disk_lsn与LSN相同时,表示所有的redo日志已刷入磁盘。 **说明**:flushed_to_disk_lsn表示的是log buffer日志被刷入到磁盘,还有一个write_lsn表示记录刷到了操作系统缓存。 ## 4.redo日志文件清理 前面已经介绍了redo日志写入log buffer以及log buffer刷入磁盘的过程。flushed_to_disk_lsn变量表示已经写到磁盘的redo日志量,lsn超出flushed_to_disk_lsn的redo日志可以从log buffer中被清理出。 本章需要思考对于已刷入磁盘的redo日志何时可以被清理。redo日志文件整体大小是固定的,且redo日志组会循环使用;因此需要一个合理的清理机制,并需要考虑两个问题: \[1\] 确认哪些redo日志可以被清理,数据被清理后不能影响持久性功能; \[2\] 何时清理,需要有足够的空间保障log buffer的redo日志记录可以被刷入redo日志文件。 ### flush链表和lsn > 对于flush链表的介绍在\[mysql系列6---Innodb的缓存\]中已详细介绍。 修改数据库时,页的改动信息会以redo日志的形式记录下,然后将涉及的改动页对应的控制块添加到flush链表中,控制块之间形成双向链表。 flush链的控制块中保存了两个修改信息oldest_modification和newest_modification,分别表示第一次修改时的lsn和最近一次修改结束后的lsn. 使用第三章中的案例,从mysql启动后,分别执行了两个mtr, mtr1占据100字节,mtr2占据150字节:  假设mtr1修改了两个页面,两个页面对应的控制块分别为"控制块1"和"控制块2";mtr2修改了一个页面,页面控制块为"控制块3", 则flush链表可以表示为:  图中O_M表示oldest_modification, N_M表示newest_modification. 页面被修改时,页面对应的控制块会加入flush链的队列头部,oldest_modification记录下当时的lsn, newest_modification会记录所在mtr结束时的lsn; 当后续这个页面再次被修改时,仅修改newest_modification的值,oldest_modification和位置不会发生变化,即控制块是按照第一次修改的时间按从大到小的顺序排列的。 **因此,位于队列尾部的控制块的oldest_modification是整个flush链中最小的lsn(后续用最小LSN表示).** 另外,当脏页(修改过的数据页)被写入到磁盘后,数据页对应的控制块会从flush链中删除,因此redo日志文件中lsn小于\*\*"最小LSN"**可以被直接删除。 由此可以得出一个结论:判断redo日志文件中日志是否可被删除的条件是,**脏页是否已经刷到磁盘了**,即redo日志的lsn是否小于** 最小LSN\*\*。 由于全局LSN根据写入log buffer的mtr大小进行递增,且log buffer与redo日志文件以log block为单位进行存储;因此可根据lsn计算出对应redo日志在文件组中的偏移量。 ### checkpoint 上一节提到了一个\*\*"最小LSN"**的概念,mysql使用checkpiont_lsn表示。刷新一次checkpiont_lsn的操作被称为一次chekpoint操作,会计算出当前系统的最小LSN并赋值给checkpiont_lsn: \[1\] 取出redo文件组的第一个文件的管理信息, 得到较大的checkpoint_no和对应的checkpoint_lsn; \[2\] 对checkpoint_no加1,并写入文件; \[3\] 将当前** "最小LSN"\*\*赋值给checkpoint_lsn,并写入到文件中。 经历checkpoint后,redo日志文件中可被清理的部分会被标记出,清理操作由后台线程完成。 ## 5.崩溃恢复 从崩溃中恢复是redo日志保证持久性的关键所在。数据库崩溃后,根据redo日志进行数据库恢复可分为三个步骤:确定恢复起点、确定恢复终点、执行恢复流程。 ### 5.1 确认恢复起始点 章节1.2中介绍过,redo日志文件组中第一个文件的管理信息中在两个block都存储了checkpoint_no和checkpoint_lsn的信息。其中较大的checkpoint_lsn表示上一次checkpoint操作对应的lsn,即**最小LSN** . 由于lsn小于checkpoint_lsn的脏页已经被刷盘,可通过checkpoint_lsn可以计算出在redo日志组中的偏移量,使用该偏移量作为数据恢复的起点。  上图中控制块3表示未刷盘的最早被修改的页的控制块,其oldest_modification是最小的lsn, 用作数据恢复的起点。 ### 5.2 确认恢复终点 章节1.2中介绍过每个log block的header存储了log_block_hdr_data_len属性,记录当前页使用的字节数。如果log_block_hdr_data_len等于512,说明当前页已满,小于512说明当前页未满,未满的block表示最新的数据。通过未满的block的log_block_hdr_no可计算出在redo日志组中的偏移量,作为数据恢复的终点。  上图中redo日志文件的第100个log block已满,而第101个未满,101最后一个redo日志的lsn作为日志恢复终点的lsn. ### 5.3 恢复流程 恢复的时候需要注意一个问题:checkpoint_lsn之前的redo日志对应的脏页已经刷入磁盘了,但checkpoint_lsn之后的可能已刷盘也可能没有,需要区分对待。 每个数据页都有一个File Header文件头,通过**file_page_lsn** 属性记录了最近一次修改页面时的lsn值(对应flush链中的newest_modification)。在checkpoint之后刷入磁盘的脏页,file_page_lsn大于checkpoint_lsn;对于这部分数据页,恢复时直接跳过;file_page_lsn和数据页的内容可参考[mysql系列5---Innodb的缓存](https://shengyu.blog.csdn.net/article/details/144168834)。 通过redo日志组的恢复起点偏移量和终点偏移量可以得到一段redo日志,再根据file_page_lsn过滤掉一部分,得到一个redo日志数组。 这些redo日志数组可能涉及不同表空间的不同数据页的修改操作,如redo-1日志修改表空间1-数据页1,redo-2修改表空间100-数据页100,redo-3修改表空间1-数据页1,redo-4修改表空间100-数据页100,... 随机写的概率比较大,可根据(表空间ID, 数据页页号)通过哈希算法对redo日志进行映射分组,然后按照组依次执行,将随机IO转为多个连续IO从而提高系统效率。