MySQL学习(14)redo日志

发布时间 2023-11-09 18:37:59作者: 哪过晓得

前言

InnoDB存储引擎以页为单位从磁盘中加载到内存中,进行数据的管理。我们进行增删改查操作本质上是访问页面,其中包括读页面、写页面、创建新页面等操作。在访问页面之前,需要将页从磁盘中加载到Buffer Pool中才可以访问。在Buffer Pool中修改了数据后,会加入到flush链表中,但是flush链表并不能及时刷新到次盘。如果这个时候系统发生崩溃,内存中的数据丢失,未来的及从flush链表中刷新到磁盘的页也就没了。一个事务执行完成后,为了保证事务的持久性原则,在事务提交后,即使系统发生崩溃,这个事务造成的影响也不能丢失。

若在事务提交时主动将脏页刷新到磁盘,稍改动一下就要刷新整个页,这也会造成资源浪费,且随机I/O非常耗时。

redo日志是什么

没有必要每次都将一整个页刷新到磁盘,只需要记住所做的修改即可。例如第0号表空间第10页偏移量为1000字节处将1修改为2。重启之后,只需要按照日志记录的步骤重新更新一下数据页,就可以恢复出未来的及写入磁盘的脏页的状态,这也意味着保证了持久性的要求。

redo日志就是记录了事务对数据库进行了哪些修改的日志数据。事务提交时立即刷新到磁盘的是redo日志,不是Buffer Pool中的脏页。

redo日志的作用

当已经提交的事务对数据库产生的修改能够永久生效,及时发生崩溃,系统重启后也能恢复过来。又不需要每次将页刷新到磁盘造成大量开销。

redo日志的优点:

  • redo日志占用的空间非常小。

  • redo日志是按照产生的顺序写入磁盘的,也就是使用顺序I/O。

日志格式

通用的redo日志格式:

通用redo日志结构

  • type:redo日志的类型,一共约有53种。

  • space ID:表空间ID。

  • page number:页号。

  • data:这条redo日志的具体内容。

简单日志

有些操作修改的数据非常简单,使用redo日志记录它的操作时,甚至只需要记录内存中某一位置修改为某个值。这种简单的redo日志称为物理日志。有以下几种:

  • MLOG_1BYTE(type=1):表示在页面的某个偏移量处写入1字节的redo日志类型。

  • MLOG_2BYTE(type=2):表示在页面的某个偏移量处写入2字节的redo日志类型

  • MLOG_4BYTE(type=4):表示在页面的某个偏移量处写入4字节的redo日志类型。

  • MLOG_8BYTE(type=8):表示在页面的某个偏移量处写入8字节的redo日志类型。

  • MLOG_WRITE_STRING(type=30):表示在页面的某个偏移量处写入了一个字节序列。

MLOG_1BYTE结构如图所示:

确定字节数redo日志结构

MLOG_WRITE_STRING类型的redo日志表示写入了一个字节序列,但是不确定写入了多少,所以需要添加一个表示字节长度的len字段,结构如图所示:

不确定字节数redo日志结构

复杂日志

一般情况,一条INSERT语句可能产生多处修改:

  • 表中有多少个索引,一条INSERT语句就可能修改多少个B+树。

  • 每一颗B+树在修改时,既可能修改叶子节点,又可能修改非叶子节点,还可能创建新的页面(空间不够新申请页面、页分裂、或非叶子节点添加目录项)。

在把一条记录插入到一个页面时,需要修改的地方太多。将每一段修改都记录一条redo日志,又或是对修改的起始位置到修改的终止位置之间全部记录,这两种方法都非常浪费空间。对于这种情况可使用一下日志类型:

  • MLOG_REC_INSERT(type=9):表示在插入一条使用非紧凑行格式(REDUNDANT)的记录时,redo日志的类型。

  • MLOG_COMP_REC_INSERT(type=38):表示在插入一条使用使用紧凑行格式(COMPACT、DYNAMIC、COMPRESSED)的记录时,redo日志的类型。

  • MLOG_COMP_PAGE_CREATE(type=58):表示在创建一个存储紧凑行格式记录的页面时,redo日志的类型。

  • MLOG_CPMP_REC_DELETE(type=42):表示在删除一条使用紧凑行格式的记录时,redo日志的记录。

  • MLOG_COMP_LIST_START_DELETE(type=44):表示从某条给定记录开始删除页面中一系列使用紧凑行格式的记录时,redo日志的类型。

  • MLOG_COMP_LIST_END_DELETE(type=43):表示删除一系列记录,到此结束时,redo日志的类型。

  • MLOG_ZIP_PAGE_COMPRESS(type=51):表示在压缩一个数据页时,redo日志的类型。

这些较为复杂的redo日志既包含物理层面的意思,也包括逻辑层面的意思。从物理层面,这些日志指明了表空间号页号,甚至是偏移量;从逻辑层面,在系统崩溃后重启时,并不能根据这些日志中的记载,在页面中的某个偏移量处恢复某个数据,而是需要调用一些事先准备好的函数,在执行这些函数后才能恢复崩溃前的数据。

以MLOG_COMP_REC_INSERT为例:

MLOG_COMP_REC_INSERT日志结构示意图

  • n_uniques:表示在一条记录中需要多少个字段才能保证记录的唯一性。对于聚簇索引,n_uniques为主键列数,对于二级索引,n_uniques为索引列+主键列数。唯一二级索引的值可能为NULL,所以也包含列数。

  • field1_len~fieldn_len:代表该记录若干个字段占用存储空间的大小。需要注意的是,这里无论该字段的类型是固定长度类型〈比如INT),还是可变长度类型(比如 VARCHAR(M))。该字段占用的存储空间大小始终要写入redo日志中。

  • offset:代表该记录的前一条记录在页面中的地址。为啥要记录前一条记录的地址呢?这是因为每向数据页插入一条记录都需要修改该页面中维护的记录链表。每条记录的记录头信息中都包含一个名为next_record的属性,所以在插入新记录时,需要修改前一条记录的next_record属性。

 

MTR

一组redo日志

很多情况一个操作会产生一组redo日志,这些redo日志不可分割。具有原子性的要求,系统崩溃后重启时,要么将这一组redo日志全部恢复,要么全部都不恢复。对于需要保证原子性的操作产生的redo日志上,有一些特殊的处理:

  • 一个需要保证原子性的操作产生多条不可分割的redo日志

在该组最后一条redo日志上加一条特殊类型的redo日志,表示结束。该类型的redo日志叫做MLOG_MULTI_REC_END,结构很简单,只有一个type字段(type=31)。

mysql_1910

一个需要保证原子性的操作产生的多条redo日志,必须以MLOG_MULTI_REC_END类型的redo日志结束。在系统崩溃后重启时,只有解析到MLOG_MULTI_REC_END类型的redo日志时才会恢复这一组redo日志,否则直接放弃掉前面全部的redo日志。

mysql_1911

  • 一个需要保证原子性的操作产生一条 redo日志

redo日志的type字段占用1字节,但是实际表示类型只需要数值127就足够,也就是7个比特位。还有1位来表示该redo日志是单一的redo日志,还是属于一组redo日志中的一条。type字段的第1位为1时,表示这个需要保证原子性的操作产生了一条单一的redo日志,就是这条日志;type字段的第1位为0时,表示这个需要保证原子性的操作产生了一组redo日志,这条日志属于其中之一,必须遇到MLOG_MULTI_REC_END类型的redo日志才算完整。

Mini-Transaction

对底层页面进行一次原子访问的过程称为一个Mini-Transaction(MRT)。向B+树插入一条记录的过程算是一个MTR。一个MTR可以包含一组redo日志,在进行崩溃恢复时,需要把这一组redo日志当做一个整体来处理。

B+树插入一条记录时,可能会产生多条redo日志,包括新建页面、新增记录、新增目录项等。

一个事务可以包含多条语句,一条语句可以包含多个MTR,一个MTR可以包含多条redo日志。

mysql_1913

 

redo log block

为了更好的管理,MySQL把通过MTR生成的redo日志都放在了大小为512字节的块中,称之为redo log block。

数据结构

其实真正用来存储日志信息的区域是496字节,其余为管理信息存放在log block header和log block trailer的区域中。一个完整的redo log block如下图所示:

redo log block结构

  • log block header存放的信息如下:

    • LOG_BLOCK_HDR_NO:表示block的唯一编号。

    • LOG_BLOCK_HDR_DATA_LEN:表示block中已经使用了多少字节;初始值为12,因为log block body从12字节开始;该值为512时表示block已经被写满。

    • LOG_BLOCK_FIRST_REC_GROUP:表示block中第一个MTR产生的redo日志记录组的偏移量,也就是MTR第一条redo日志的偏移量。如果一个MTR横跨了多个block,那么最后一个block中的LOG_BLOCK_FIRST_REC_GROUP值就表示这个MTR对应的redo日志组结束的地方(下一个MTR产生的redo日志组开始的地方),中间的block没有LOG_BLOCK_FIRST_REC_GROUP值。

    • LOG_BLOCK_CHECKPOINT_NO:表示checkpoin的序号。

  • log block body存放的是redo日志

  • log block trailer

    • LOG_BLOCK_CHECKSUM:表示该block的校验。

在初次使用一个block时,系统会为这个block分配一个唯一编号,就是LOG_BLOCK_HDR_NO。这个分配的值是根据如下算法得出来:

 

 
((lsn / 512) & 0x3FFFFFFF) + 1

mysql_1942

0x3FFFFFFF对应的32位二进制数的前2个比特为0,后30个比特为1.一个二进制位与0进行与运算的结果肯定是0,一个二进制位与1进行与运算的结果就是原值。让一个数与0xFFFFFFF进行与运算的意思就是要将该值的前2个比特置为0,这样该值就一定小于或等于0x3FFFFFFF了。无论lsn多大,((lsn / 512) & 0x3FFFFFFF)的值肯定在0到0x3FFFFFFF之间,再加1,这个范围就是1到0x40000000之间。0x40000000就是230,也就是1G。系统能产生的不重复的LOG_BLOCK_HDR_NO值最多有1G个。MySQL规定日志文件组中包含的所有文件大小的综合不得超过512GB,一个block大小是512字节,也就是说redo日志文件组中包含的block块最多为1G个,这些不重复的编号值够用了。

LOG_BLOCK_HDR_NO值的第一个比特称为flush bit。如果为1,表示本block是在将log buffer中的block刷新到磁盘的某次操作中时,第一个被刷入的block。

redo日志缓冲区——redo log buffer

为了解决磁盘写入过慢的问题,redo log一开始是存储在内存中的。MySQL服务启动时向操作系统申请了一片连续的内存空间,称为redo log buffer。这篇空间被划分为若干个连续的redo log block。

redo log buffer结构

redo log buffer的大小由启动项innodb_log_buffer_size设置,默认大小为16MB。

SHOW VARIABLES LIKE 'innodb_log_buffer_size';

redo日志写入redo log buffer

redo日志写入到redo log buffer时是顺序写入,也就是从前面的block开始依次写入,用完了block的空间后,紧接着写入到下一个block。MySQL维护了一个全局变量buf_free,用来表示redo log buffer中空闲位置开始处的偏移量,用来指明后续写入的redo日志应该写到redo log buffer中的什么地方。

redo日志写入redo log buffer

一个MTR会产生多条redo日志,这些redo日志是一个不可分割的组,并不是每产生一条redo日志就立刻插入到redo log buffer中,而是将每个MTR运行中产生的redo日志暂存到一个地方,当MTR结束的时候,再将这些redo日志全部复制到redo log buffer中。

每个事务可能由多个MTR组成,不同的事务是可能并发执行的,每当一个MTR执行完成时,伴随该MTR生成的一组redo日志就需要被复制到redo log buffer中,也就是说不同事务的redo日志可能是交替写入log buffer中的,但是同一个MTR的redo日志是连续写入的。

mysql_1919

 

刷盘机制

redo log buffer的空间有限,并且系统崩溃会导致内存数据丢失,存储在redo log buffer中的redo日志必须要刷到磁盘上存储。也不是产生了redo日志就立刻刷新到磁盘,而是遵循下列规则:

  • redo log buffer空间不足时。一般占用50%左右时,就需要把这些日志刷新到磁盘。

  • 事务提交时。事务提交时可以不立刻将Buffer Pool中的页刷新到磁盘,但是必须立刻把事务产生的redo日志刷新到磁盘,否则无法保证持久性。

  • 将某个脏页刷新到磁盘前,会保证先将该脏页对应的redo日志刷新到磁盘。

  • 后台进程异步将redo log buffer中的redo日志刷新到磁盘,约每秒一次。

  • 正常关闭服务器时,会将log buffer中的redo日志全部刷新到磁盘。

  • 做checkpoint时。

通过修改innodb_flush_log_at_trx_commit系统变量的值,可以设定每次在事务提交时主动将redo日志写入到磁盘。innodb_flush_log_at_trx_commit变量默认值为1,有3个可选值:

  • 0:表示事务提交时不立即向磁盘同步redo日志。

  • 1:事务提交时需要将redo日志同步到磁盘。

  • 2:事务提交时需要将redo日志写到操作系统的缓冲区中,不保证及时刷新到磁盘。MySQL服务崩溃后可从操作系统中获取存储的redo日志;但若操作系统崩溃,就凉了。

SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';

log file

文件组

MySQL数据目录下默认有名为ib_logfile0和ib_logfile1的文件,这就是存储redo日志的文件。

image-20231109112524742

通过下面这几个启动项可以设置log file:

  • innodb_log_group_home_dir:指定redo日志文件存放的目录。

  • innodb_log_file_size:指定每个redo日志文件的大小,MySQL 5.7.22默认48MB。

  • innodb_log_files_in_group:指定redo日志文件的个数,默认为2,最大值为100.

磁盘上的redo日志文件不止一个,而是一个日志文件组,从图中看到的ib_logfile0和ib_logfile1.如果ib_logfile0写满了,就接着ib_logfile1写,依次类推。如果最后一个文件写满了,就将第一个文件覆盖掉,继续写入新的redo日志。

redo日志文件组

文件格式

redo日志文件本质上是将redo log buffer中的redo log block写入到文件中。所以在redo日志文件中,也有由若干个512字节的block组成。在redo日志文件组中,每一个文件的大小都相同,格式也一样,都是有下面两个部分组成:

  • 前2048个字节(前4个block)用来存储一些管理信息。

  • 从第2048字节往后的空间用来存储redo log buffer中的block镜像。

redo日志文件组结构

每个redo日志文件都是从第2048字节开始存储redo日志的。前2048个字节存储的4个block具备管理的作用。前4个block如下图所示:

redo日志文件前4个block组成

第一个block称为log file header,描述了该redo日志文件的一些整体属性。

属性名长度(字节)描述
LOG_HEADER_FORMAT 4 redo日志的版本,MySQL 5.7.22中该值始终为1
LOG_HEADER_PAD1 4 填充,无实际意义
LOG_HEADER_START_LSN 8 标记本redo日志文件偏移量为2048字节处对应的lsn值
LOG_HEADER_CREATOR 32 一个字符串,标基本redo日志文件的创建者是谁。正常情况下该值为MySQL的版本号(如SQL 5.7.22);在使用mysqlbackup命令创建redo日志文件时,该值为“ibbackup”和创建时间
LOG_BLOCK_CHECKSUM 4 本block的校验值

第二个block称为checkpoint1,和第四个block具有相同的结构。

redo日志文件checkpoint结构

属性名大小(字节)描述
LOG_CHECKPOINT_NO 8 checkpoint编号,每执行一次checkpoint,该值加1
LOG_CHECKPOINT_LSN 8 服务器在结束checkpoint时对应的lsn值;系统在崩溃后恢复时从该值开始
LOG_CHECKPOINT_OFFSET 8 LOG_CHECKPOINT_LSN中的lsn值在日志文件组中的偏移量
LOG_CHECKPOINT_LOG_BUF_SIZE 8 服务器在执行checkpoint操作时对应的log buffer的大小
LOG_BLOCK_CHECKSUM 4 本block的校验值

注意:checkpoint的相关信息只存储在redo日志文件组的第一个日志文件中,也就是ib_logfile0文件中。

LSN

lsn(log sequence number)是系统维护的一个全局变量,用来记录自从系统开始运行,当前总共已经写入的redo日质量,初始值为8704。尽管redo日志实际被记录在block中的log block body中,但是在统计lsn时,需要包括log block header和log block trailer。

每一组由MTR产生的redo日志都有一个唯一的lsn值与其对应;lsn值越小说明redo日志产生的越早。

flushed_to_disk_lsn

redo日志先写在redo log buffer中,再刷新到磁盘的。全局标量buf_next_to_write用来表示当前redo log buffer已经写入到磁盘的地址偏移量。有一个全局变量flushed_to_disk_lsn记录了已写入磁盘中的redo日志的总量。

lsn记录了redo日志的写入量,统计的是log buffer。flushed_to_disk_lsn记录了已写入磁盘的量。系统在第一次启动时,flushed_to_disk_lsn值和lsn值是相等的,都是8704。随着系统的运行,不断地有redo日志被写入到redo log buffer中,但是redo log buffer中的日志并没有实时全部刷新到磁盘。此时lsn值就大于buf_next_to_write值。

运行过程中buf_next_to_write和buf_free

当有新的redo日志写入到log buffer时,lsn值会增长,但flushed_to_disk_lsn值不变;随着log buffer中的日志被刷新到磁盘上,flushed_to_disk_lsn的值开始增长。如果两者相等,说明log buffer中的所有redo日志已经全部刷新到磁盘中。

lsn和redo日志文件组中的偏移量的对应关系

lsn记录的是redo日志量的总和,其中包括block中的header和trailer。redo log buffer中的redo日志是什么样的,写入到文件还是原样。所以redo日志的lsn值与其在redo日志文件中的偏移量存在对应的关系。

mysql_1931

初始的lsn值为8704,对应的redo日志文件组偏移量是2048,然后累加即可。要注意的是,每一个redo日志文件的头4个block占用的大小。

flush链表中的lsn

一次MTR操作结束时,会将产生的一组不可分割的redo日志写入到redo log buffer中,除此之外,还要将修改过的页加入到Buffer Pool中的flush链表中。

当第一次修改某个已经加载到Buffer Pool中的页时,会将其对应的控制块插入到flush链表头部。也就是说,flush链表中的脏页时按照页面第一次修改时间进行排序的。缓冲页对应的控制块中记录了两个属性:

  • oldest_modification:表示第一次修改Buffer Pool中的某个缓冲页时,修改该页面的MTR开始时对应的lsn值。

  • newest_modification:表示最近一次修改这个页面的MTR结束时对应的lsn值。

flush链表中的脏页按照第一次修改发生的事件顺序进行排序,也就是按照oldest_modification代表的lsn值进行排序;被多次更新的页面不会重复插入到flush链表中,但是会更新newest_modification值。

修改页a:

![mysql_1933](C:\Users\yangh\01_Project\MySQL学习\临时\mysql_1933.png)

修改页b、页c:

mysql_1934

修改页b、页d:

mysql_1935

查看系统中的各种lsn值

使用SHOW ENGINE INNODB STATUS命令查看当前InnoDB存储引起中各种lsn值的情况。

SHOW ENGINE INNODB STATUS;
=====================================
2023-11-02 08:01:00 140257056925440 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 30 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 1863 srv_active, 0 srv_shutdown, 11594113 srv_idle
srv_master_thread log flush and writes: 0
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 18514
OS WAIT ARRAY INFO: signal count 688964
RW-shared spins 0, rounds 0, OS waits 0
RW-excl spins 0, rounds 0, OS waits 0
RW-sx spins 0, rounds 0, OS waits 0
Spin rounds per wait: 0.00 RW-shared, 0.00 RW-excl, 0.00 RW-sx
------------
TRANSACTIONS
------------
Trx id counter 136421
Purge done for trx's n:o < 136421 undo n:o < 0 state: running but idle
History list length 0
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421732180061592, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421732180060736, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421732180059880, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
--------
FILE I/O
--------
I/O thread 0 state: waiting for completed aio requests (insert buffer thread)
I/O thread 1 state: waiting for completed aio requests (log thread)
I/O thread 2 state: waiting for completed aio requests (read thread)
I/O thread 3 state: waiting for completed aio requests (read thread)
I/O thread 4 state: waiting for completed aio requests (read thread)
I/O thread 5 state: waiting for completed aio requests (read thread)
I/O thread 6 state: waiting for completed aio requests (write thread)
I/O thread 7 state: waiting for completed aio requests (write thread)
I/O thread 8 state: waiting for completed aio requests (write thread)
I/O thread 9 state: waiting for completed aio requests (write thread)
Pending normal aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0] ,
 ibuf aio reads:, log i/o's:, sync i/o's:
Pending flushes (fsync) log: 0; buffer pool: 0
861308 OS file reads, 926566 OS file writes, 340279 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 0.00 writes/s, 0.00 fsyncs/s
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 530, seg size 532, 4321 merges
merged operations:
 insert 104158, delete mark 0, delete 0
discarded operations:
 insert 0, delete mark 0, delete 0
Hash table size 34679, node heap has 1 buffer(s)
Hash table size 34679, node heap has 2 buffer(s)
Hash table size 34679, node heap has 19 buffer(s)
Hash table size 34679, node heap has 1 buffer(s)
Hash table size 34679, node heap has 1 buffer(s)
Hash table size 34679, node heap has 1 buffer(s)
Hash table size 34679, node heap has 10 buffer(s)
Hash table size 34679, node heap has 2 buffer(s)
0.00 hash searches/s, 0.00 non-hash searches/s
---
LOG
---
Log sequence number          2301683137
Log buffer assigned up to    2301683137
Log buffer completed up to   2301683137
Log written up to            2301683137
Log flushed up to            2301683137
Added dirty pages up to      2301683137
Pages flushed up to          2301683137
Last checkpoint at           2301683137
552807 log i/o's done, 0.00 log i/o's/second
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 137035776
Dictionary memory allocated 619909
Buffer pool size   8192
Free buffers       1023
Database pages     7132
Old database pages 2613
Modified db pages  0
Pending reads      0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 532550, not young 2012340
0.00 youngs/s, 0.00 non-youngs/s
Pages read 861134, created 50731, written 297161
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 7132, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
--------------
ROW OPERATIONS
--------------
0 queries inside InnoDB, 0 queries in queue
0 read views open inside InnoDB
Process ID=1, Main thread ID=140256717575936 , state=sleeping
Number of rows inserted 5010025, updated 49, deleted 0, read 460244302
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
Number of system rows inserted 234, updated 4592, deleted 9959, read 137873
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
----------------------------
END OF INNODB MONITOR OUTPUT
============================
INNODB STATUS
  • Log sequence number:表示系统中的lsn值,即写入的所有redo日质量。

  • Log flushed up to:表示flushed_to_disk_lsn的值,即已写入磁盘的redo日志量。

  • Pages flushed up to:表示flush链表中最早修改的那个页面对应的oldest_modification。

  • Last checkpoint:表示当前系统的checkpoint_lsn值。

checkpoint

当redo日志对应的脏页已经刷新到磁盘中,这些redo日志也就没有存在的价值了,即使发生系统崩溃,重启后也不需要redo日志来进行恢复。这些redo日志占用的磁盘空间随时可以被重用。

MySQL定义了一个全局变量checkpoint_lsn,用来表示当前系统中可以被覆盖的redo日志总量是多少,初始值8704。

每当flush链表中的脏页被刷新到磁盘上,对应的redo日志就可以被覆盖了,此时checkpoint_lsn值增加,这个过程就是一次checkpoint。要注意的是,将脏页刷新到磁盘和checkpoint是不同线程异步执行。

执行步骤步骤

执行一次checkpoint分为两个步骤:

步骤1. 计算当前系统中可以被覆盖的redo日志对应的lsn值最大是多少。

找到flush链表中的尾节点,其记录的oldest_modification值就是当前内存中最早开始修改的redo日志lsn,也就是说redo日志对应的lsn小于这个值,都可以被覆盖掉。将这个节点的oldest_modification赋值给checkpoint_lsn。

步骤2. 将checkpoint_lsn与对应的redo日志文件组偏移量以及此次checkpoint的编号写到日志文件的管理信息checkpoint1和checkpoint2中。

MySQL中维护了一个变量叫checkpoint_no,用来统计目前系统执行了多少次checkpoint;每执行一次checkpoint,该变量的值加1。通过计算得到checkpoint_lsn对应的redo日志文件组中的偏移量checkpoint_offset,然后把checkpoint_no、checkpoint_lsn、checkpoint_offset这3个值写入到redo日志文件组的管理信息中。

redo日志文件组中第一个文件记录了checkpoint信息。checkpoint_no为偶数时,将这3个值写入到checkpoint1中;为奇数时,写到checkpoint2中。

mysql_1938

用户线程批量刷新脏页

一般情况下,后台线程对LRU链表和flush链表进行刷脏操作。但是,如果当前系统修改页面的操作十分频繁,导致写redo日志的操作十分频繁,系统lsn值增长过快。如果后台进程不能快速将脏页刷出,系统就无法即时执行checkpoint,可能就需要用户线程从flush链表中把尾节点的脏页刷新到磁盘,然后就可以执行checkpotin了。

崩溃恢复

redo日志的作用就是干这个的。

恢复起点

对于lsn小于checkpoint_lsn的redo日志来说,它对应的脏页已经被刷新到磁盘中了,不需要恢复这些redo日志了。lsn值不小于checkpoint_lsn的redo日志,不确定对应的脏页是否已经被刷盘(因为刷盘和checkpoint是异步执行的)。所以需要从对应的lsn值为checkpoint_lsn的redo日志开始恢复页面。

在redo日志文件组第一个文件的管理信息中,有两个block存储了checkpoint_lsn,分别是checkpoint1和checkpoint2。比较这两个block中的checkpoint_no大小,选择值更大的,说明这个checkpoint_lsn为最近一次执行checkpoint所记录的lsn。这样就拿到了最近一次checkpoint_lsn以及在对应的redo文件组中的偏移量checkpoint_offset。

恢复终点

redo日志是顺序写入的,写满了一个block就继续写下一个block。block的log block header中有一个名为LOG_BLOCK_HDR_DATA_LEN的属性,记录了当前block使用了多少空间。如果属性值不为512,那就说明这是最新的redo日志所处的block,它就是崩溃恢复中需要扫描的最后一个block。

mysql_1939

发生崩溃后恢复时,从checkpoint_lsn在日志文件组中对应的偏移量开始扫描,直到LOG_BLOCK_HDR_DATA_LEN值不为512的block为止

如何恢复

在确定恢复起点和恢复终点后,就要扫描这个区间范围内的redo日志,并进行恢复。

在开始恢复时,按照redo日志的顺序依次扫描各条redo日志,按照日志中记载的内容将对应的页面恢复过来。

MySQL为了更快的恢复,做了以下处理:

  • 使用哈希表

根据redo日志的space ID和page number计算出哈希值,把space ID和page number相同的rredo日志放到哈希表的同一个槽中。如果有多个space ID和page number相同的redo日志,就按照他们之间使用链表连接起来(必须按redo日志的生成时间顺序连接,否则会出现错乱)。通过便利哈希表,一次将一个页面中所有的redo日志都恢复好,避免了很多读取页面的随机I/O。

  • 跳过已经刷新到磁盘中的页面

每一个页面的File Header部分中有一个属性FIL_PAGE_LSN,记录了最后一次修改页面时对应的lsn值。如果在执行了某次checkpoint后,有脏页被刷新到磁盘中,那么该页对应的FIL_PAGE_LSN代表的lsn值肯定大于checkpoint_lsn值。符合这个条件说明这个页已经刷新到磁盘中了,恢复过程中可以跳过这个页。

如果脏页还没有被刷新到磁盘中,那么磁盘中的该页FIL_PAGE_LSN的值是上一次checkpoint时的值,这比本次磁盘被读取到Buffer Pool还要更早,该值肯定比当前checkpoint_lsn小。如果脏页已经被刷新到磁盘了,FIL_PAGE_LSN就更新为了本次的checkpoint的newest_modification值,肯定比checkpoint_lsn大。

 

阅读学习《MySQL是怎样运行的》小孩子4919