208_mysql_innodb_5_Innodb_redo2
Redo 日志
DML(8.0+ DDL)操作导致的页面变化,均需要记录Redo日志(WAL模式) 系统奔溃重启时需要重新更新数据页,所以相关记录也被称之为重做日志redo log,
特点:
1 redo日志占用空间小
2 redo日志是顺序写入磁盘的,(一条SQL 产生若干redo日志)这些日志顺序写入磁盘
一 Redo 日志格式
redo日志本质上只是记录了一下事务对数据库做了哪些修改
InnoDB对事务对数据库的不同修改场景定义了多种类型的redo日志 (page number 页号, spaceid 表空间id)
我们需要为这个页面的修改记录一条redo日志,这种情况下,redo日志中只需要记录一下在某个页面的某个偏移量处修改了几个字节的值,具体被修改的内容是啥就好了,InnoDB把这种极其简单的redo日志称之为物理日志,并且根据在页面中写入数据的多少划分了几种不同的redo日志类型:
1.1简单的redo日志类型
无显示主键/Unique键 - row_id隐藏列 -> 系统中维护全局自增变量(赋给row_id)-> 每当该变量到256倍数会被刷到系统表空间的页号为7的页面中一个称之为Max Row ID的属性处
当系统启动时,会将上边提到的Max Row ID属性(存储空间是8个字节,)加载到内存中,将该值加上256之后赋值给我们前边提到的全局变量(因为在上次关机时该全局变量的值可能大于Max Row ID属性值)。
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):表示在页面的某个偏移量处写入一串数据。
我们上边提到的Max Row ID属性实际占用8个字节的存储空间,所以在修改页面中的该属性时,会记录一条类型为MLOG_8BYTE的redo日志
MLOG_WRITE_STRING类型的redo日志表示写入一串数据,但是因为不能确定写入的具体数据占用多少字节,所以需要在日志结构中添加一个len字段
1.2 复杂一些的redo日志类型
有时候执行一条语句会修改非常多的页面,包括系统数据页面和用户数据页面(用户数据指的就是聚簇索引和二级索引对应的B+树)
以一条INSERT语句为例,它除了要向B+树的页面中插入数据,也可能更新系统数据Max Row ID的值,表中包含多少个索引,一条INSERT语句就可能更新多少棵B+树
解决方案
MLOG_REC_INSERT(对应的十进制数字为9):表示插入一条使用非紧凑行格式的记录时的redo日志类型。
MLOG_COMP_REC_INSERT(对应的十进制数字为38):表示插入一条使用紧凑行格式的记录时的redo日志类型。
MLOG_COMP_PAGE_CREATE(type字段对应的十进制数字为58):表示创建一个存储紧凑行格式记录的页面的redo日志类型。
MLOG_COMP_REC_DELETE(type字段对应的十进制数字为42):表示删除一条使用紧凑行格式记录的redo日志类型。
MLOG_COMP_LIST_START_DELETE(type字段对应的十进制数字为44):表示从某条给定记录开始删除页面中的一系列使用紧凑行格式记录的redo日志类型。
MLOG_COMP_LIST_END_DELETE(type字段对应的十进制数字为43):与MLOG_COMP_LIST_START_DELETE类型呼应,表示删除一系列记录直到MLOG_COMP_LIST_END_DELETE类型的redo日志对应的记录为止
MLOG_ZIP_PAGE_COMPRESS(type字段对应的十进制数字为51):表示压缩一个数据页的redo日志类型。
物理层面看,这些日志都指明了对哪个表空间的哪个页进行了修改
逻辑层面看,在系统奔溃重启时,并不能直接根据这些日志里的记载,将页面内的某个偏移量处恢复成某个数据,而是需要调用一些事先准备好的函数,执行完这些函数后才可以将页面恢复成系统奔溃前的样子
MLOG_COMP_REC_INSERT的redo日志注意点:
图中n_uniques的值的含义是在一条记录中,需要几个字段的值才能确保记录的唯一性,这样当插入一条记录时就可以按照记录的前n_uniques个字段进行排序。
对于聚簇索引来说,n_uniques的值为主键的列数,
对于其他二级索引来说,该值为索引列数+主键列数。这里需要注意的是,唯一二级索引的值可能为NULL,所以该值仍然为索引列数+主键列数。
field1_len ~ fieldn_len代表着该记录若干个字段占用存储空间的大小
这里不管该字段的类型是固定长度大小的(比如INT),还是可变长度大小(比如VARCHAR(M))的,该字段占用的大小始终要写入redo日志中
offset该记录的前一条记录在页面中的地址。这是因为每向数据页插入一条记录,都需要修改该页面中维护的记录链表,每条记录的记录头信息中都包含一个称为next_record的属性,所以在插入新记录时,需要修改前一条记录的next_record属性
end_seg_len 值可以间接的计算出一条记录占用存储空间的总大小(由额外信息和真实数据这两部分组成)
mismatch_index的值也是为了节省redo日志的大小而设立的忽略
很显然这个类型为MLOG_COMP_REC_INSERT的redo日志并没有记录PAGE_N_DIR_SLOTS的值修改为了啥,PAGE_HEAP_TOP的值修改为了啥,PAGE_N_HEAP的值修改为了啥等等这些信息,而只是把在本页面中插入一条记录所有必备的要素记了下来,之后系统奔溃重启时,服务器会调用相关向某个页面插入一条记录的那个函数,而redo日志中的那些数据就可以被当成是调用这个函数所需的参数,在调用完该函数后,页面中的PAGE_N_DIR_SLOTS、PAGE_HEAP_TOP、PAGE_N_HEAP等等的值也就都被恢复到系统奔溃前的样子了,这就是所谓的逻辑日志的意思。
二 Mini-Transaction
-
组的形式写入redo日志
语句在执行过程中可能修改若干个页面。在执行语句的过程中产生的redo日志被划分成了若干个不可分割的组
向某个索引对应的B+树中插入一条记录的这个过程必须是原子的,不能说插了一半之后就停止了,有的需要保证原子性的操作会生成多条redo日志
redo日志如何划分到一个组里边儿呢?
该组中的最后一条redo日志后边加上一条特殊类型的redo日志,该类型名称为MLOG_MULTI_REC_END,type字段对应的十进制数字为31,该类型的redo日志结构很简单,只有一个type字段:
恢复时,只有当解析到类型为MLOG_MULTI_REC_END的redo日志,才认为解析到了一组完整的redo日志,才会进行恢复。否则的话直接放弃前边解析到的redo日志
2 Mini-Transaction的概念
把对底层页面中的一次原子访问的过程称之为一个Mini-Transaction,简称mtr,一个所谓的mtr可以包含一组redo日志,在进行奔溃恢复时这一组redo日志作为一个不可分割的整体, 比如
- 修改一次Max Row ID的值算是一个Mini-Transaction,
- 向某个索引对应的B+树中插入一条记录的过程也算是一个Mini-Transaction
三 redo 日志的写入过程
3.1 redo log block
通过mtr生成的redo日志都放在了大小为512字节的页中。为了和表空间中的页做区别,我们这里把用来存储redo日志的页称为block(页和block差不多)
redo日志都是存储到占用496字节大小的log block body中,log block header和log block trailer存储的是一些管理信息
- log block header如下:
LOG_BLOCK_HDR_NO:每一个block都有一个大于0的唯一标号,本属性就表示该标号值。
LOG_BLOCK_HDR_DATA_LEN:已经使用了多少字节,初始值为12 如果log block body已经被全部写满,那么本属性的值被设置为512。
LOG_BLOCK_FIRST_REC_GROUP:该block中第一个mtr生成的redo日志记录组的偏移量(其实也就是这个block里第一个mtr生成的第一条redo日志的偏移量)
redo log record 一条redo日志记录,
redo log record group:mtr包括多条redo日志记录所形成的组
LOG_BLOCK_CHECKPOINT_NO:表示所谓的checkpoint的序号 (checkpoint_lsn)
- log block trailer中属性的意思如下:
LOG_BLOCK_CHECKSUM:表示block的校验值,用于正确性校验
3.2 redo日志缓冲区
写入redo日志时也不能直接直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为redo log buffer的连续内存空间,redo日志缓冲区,简称为log buffer。这片内存空间被划分成若干个连续的redo log block
通过 show VARIABLES like "%innodb_log_buffer_size%" 查看和修改 默认16M
3.3 redo日志写入log buffer
向log buffer中写入redo日志的过程是顺序的,也就是先往前边的block中写,当该block的空闲空间用完之后再往下一个block中写。
当我们想往log buffer中写入redo日志时,第一个遇到的问题就是应该写在哪个block的哪个偏移量处,
InnoDB提供了buf_free的全局变量,该变量指明后续写入的redo日志应该写入到log buffer中的哪个位置
mtr执行过程中可能产生若干条redo日志,这些redo日志是一个不可分割的组,当该mtr结束的时候,将过程中产生的一组redo日志再全部复制到log buffer中
mtr产生的一组redo日志占用的存储空间可能不一样,有的mtr产生的redo日志量很少,比如mtr_t1_1、mtr_t2_1就被放到同一个block中存储,
有的mtr产生的redo日志量非常大,比如mtr_t1_2产生的redo日志甚至占用了3个block来存储
四 redo 日志文件组
innodb_log_group_home_dir 指定了redo日志文件所在的目录,默认值就是当前的数据目录。
innodb_log_file_size 指定了每个redo日志文件的大小,在MySQL 5.7.21这个版本中的默认值为48MB,
innodb_log_files_in_group 该参数指定redo日志文件的个数,默认值为2,最大值为100。
redo日志文件格式
我们前边说过log buffer本质上是一片连续的内存空间,被划分成了若干个512字节大小的block。
将log buffer中的redo日志刷新到磁盘的本质就是把block的镜像写入日志文件中,所以redo日志文件其实也是由若干个512字节大小的block组成。
redo日志文件组中的每个文件大小都一样,格式也一样,循环使用redo日志文件,其实是从每个日志文件的第2048个字节开始算 都是由两部分组成:
- 前2048个字节,也就是前4个block是用来存储一些管理信息的。
- 从第2048字节往后是用来存储log buffer中的block镜像的。
redo日志文件前2048个字节 (普通block的格式 log block header、log block body、log block trialer这三个部分) 前4个block分别是 log file header checkpoint1 /2
log file header:描述该redo日志文件的一些整体属性,看一下它的结构
属性名 |
长度(单位:字节) |
描述 |
LOG_HEADER_FORMAT |
4 |
redo日志的版本,在MySQL 5.7.21中该值永远为1 |
LOG_HEADER_PAD1 |
4 |
做字节填充用的,没什么实际意义,忽略~ |
LOG_HEADER_START_LSN |
8 |
标记本redo日志文件开始的LSN值,也就是文件偏移量为2048字节初对应的LSN值 |
LOG_HEADER_CREATOR |
32 |
一个字符串,标记本redo日志文件的创建者是谁。 正常运行时该值为MySQL的版本号,比如:"MySQL 5.7.21", 使用mysqlbackup命令创建的redo日志文件的该值为"ibbackup"和创建时间。 |
LOG_BLOCK_CHECKSUM |
4 |
本block的校验值,所有block都有,我们不关心 |
checkpoint1
属性名 |
长度(单位:字节) |
描述 |
LOG_CHECKPOINT_NO |
8 |
服务器做checkpoint的编号,每做一次checkpoint,该值就加1。 |
LOG_CHECKPOINT_LSN |
8 |
服务器做checkpoint结束时对应的LSN值,系统奔溃恢复时将从该值开始。 |
LOG_CHECKPOINT_OFFSET |
8 |
上个属性中的LSN值在redo日志文件组中的偏移量 |
LOG_CHECKPOINT_LOG_BUF_SIZE |
8 |
服务器在做checkpoint操作时对应的log buffer的大小 |
LOG_BLOCK_CHECKSUM |
4 |
本block的校验值,所有block都有,我们不关心 |
五 Log Sequeue Number
自系统开始运行,就不断的在修改页面,也就意味着会不断的生成redo日志。InnoDB为记录已经写入的redo日志量,设计了一个称之为Log Sequeue Number的全局变量,日志序列号,简称lsn
初始的lsn值为8704(也就是一条redo日志也没写入时,lsn的值为8704)在统计lsn的增长量时,是按照实际写入的日志量加上占用的log block header和log block trailer来计算的
初始化log buffer时,buf_free(就是标记下一条redo日志应该写入到log buffer的位置的变量)就会指向第一个block的偏移量为12字节(log block header的大小)的地方,那么lsn值也会跟着增加12:
如果某个mtr产生的一组redo日志占用的存储空间比较小,lsn增长的量就是该mtr生成的redo日志占用的字节数
redo日志量为200字节,那么lsn就要在8716的基础上增加200,变为8916。
如果mtr产生的一组redo日志占用的存储空间比较大,也就是待插入的block剩余空闲空间不足以容纳这个mtr提交的日志时,lsn增长的量就是该mtr生成的redo日志占用的字节数加上额外占用的log block header和log block trailer的字节数
综合: LSN越小,说明redo日志产生越早
5.1 flushed_to_disk_lsn
redo日志是首先写到log buffer中,之后才会被刷新到磁盘上的redo日志文件。
buf_next_to_write全局变量,标记当前log buffer中已经有哪些日志被刷新到磁盘中了
flushed_to_disk_lsn全局变量 表示刷新到磁盘中的redo日志量 (lsn是表示当前系统中写入的redo日志量,这包括了写到log buffer而没有刷新到磁盘的日志)
该变量的值和初始的lsn值是相同的,都是8704, 随着系统运行,log buffer中redo日子暂未落盘,flushed_to_disk_lsn 就与lsn拉大了
5.2 LSN值与redo日志文件组中的偏移量的对应关系
lsn的值是代表系统写入的redo日志量的一个总和,一个mtr中产生多少日志,lsn的值就增加多少(当然有时候要加上log block header和log block trailer的大小),这样mtr产生的日志写到磁盘中时,很容易计算某一个lsn值在redo日志文件组中的偏移量 (前四个block 是头部信息,log file header checkpoint1/2 , 512/block)
5.3 Flush链表中的LSN
flush链表中的脏页按照修改发生的时间顺序进行排序,也就是按照oldest_modification代表的LSN值进行排序,被多次更新的页面不会重复插入到flush链表中,但是会更新newest_modification属性的值
mtr代表一次对底层页面的原子访问,在访问过程中可能会产生一组不可分割的redo日志,在mtr结束时,会把这一组redo日志写入到log buffer中。
在mtr结束时还有一件非常重要的事情要做,就是把在mtr执行过程中可能修改过的页面加入到Buffer Pool的flush链表。
lush链表中的脏页是按照页面的第一次修改时间从大到小进行排序的。在这个过程中会在缓存页对应的控制块中记录两个关于页面何时修改的属性
- oldest_modification:如果某个页面被加载到Buffer Pool后进行第一次修改,那么就将修改该页面的mtr开始时对应的lsn值写入这个属性。
- newest_modification:每修改一次页面,都会将修改该页面的mtr结束时对应的lsn值写入这个属性。也就是说该属性表示页面最近一次修改后对应的系统lsn值
六 CHECKPOINT
我们的redo日志文件组容量是有限的,循环使用redo日志文件组中的文件,但是这会造成最后写的redo日志与最开始写的redo日志追尾
判断某些redo日志占用的磁盘空间是否可以覆盖的依据就是它对应的脏页是否已经刷新到磁盘里
虽然mtr_1和mtr_2生成的redo日志都已经被写到了磁盘上,但是它们修改的脏页仍然留在Buffer Pool中,所以它们生成的redo日志在磁盘上的空间是不可以被覆盖的。随着系统的运行,如果页a被刷新到了磁盘,那么它对应的控制块就会从flush链表中移除,这样mtr_1生成的redo日志就没有用了,它们占用的磁盘空间就可以被覆盖掉了。
全局变量checkpoint_lsn来代表当前系统中可以被覆盖的redo日志总量是多少,这个变量初始值也是8704
比方说现在页a被刷新到了磁盘,mtr_1生成的redo日志就可以被覆盖了,我们可以进行一个增加checkpoint_lsn的操作,我们把这个过程称之为做一次checkpoint
备注: 刷脏页和checkpoint 是两码事 且在不同线程上执行
执行checkpoint的两个阶段
1)redo日志可以被覆盖,意味着它对应的脏页被刷到了磁盘,只要我们计算出当前系统中被最早修改的脏页对应的oldest_modification值,系统lsn值小于该节点的oldest_modification值时产生的redo日志都是可以被覆盖掉的,我们就把该脏页的oldest_modification赋值给checkpoint_lsn
比方说当前系统中页a已经被刷新到磁盘,那么flush链表的尾节点就是页c(当前系统中最早修改的脏页)它的oldest_modification值为8916,我们就把8916赋值给checkpoint_lsn(也就是说在redo日志对应的lsn值小于8916时就可以被覆盖掉)
2) 将checkpoint_lsn和对应的redo日志文件组偏移量以及此次checkpoint的编号写到日志文件的管理信息(就是checkpoint1或者checkpoint2)中, checkpoint_no变量目前系统做了多少次checkpoint 每做一次checkpoint,该变量的值就加1
计算一个lsn值对应的redo日志文件组偏移量是很容易的,所以可以计算得到该checkpoint_lsn在redo日志文件组中对应的偏移量checkpoint_offset,然后把这三个值都写到redo日志文件组的管理信息中
每一个redo日志文件都有2048个字节的管理信息,但是上述关于checkpoint的信息只会被写到日志文件组的第一个日志文件的管理信息中,当checkpoint_no的值是偶数时,就写到checkpoint1中,是奇数时,就写到checkpoint2中
七 用户线程批量从flush链表中刷出脏页
一般情况下都是后台的线程在对LRU链表和flush链表进行刷脏操作,这主要因为刷脏操作比较慢,不想影响用户线程处理请求。
但是如果当前系统修改页面的操作十分频繁,这样就导致写日志操作十分频繁,系统lsn值增长过快。
如果后台的刷脏操作不能将脏页刷出,那么系统无法及时做checkpoint,可能就需要用户线程同步的从flush链表中把那些最早修改的脏页(oldest_modification最小的脏页)刷新到磁盘,这样这些脏页对应的redo日志就没用了,然后就可以去做checkpoint了
八 查看系统中的各种LSN
Show engine innodb status \G
Log sequence number:代表系统中的lsn值,也就是当前系统已经写入的redo日志量,包括写入log buffer中的日志。
Log flushed up to:代表flushed_to_disk_lsn的值,也就是当前系统已经写入磁盘的redo日志量。
Pages flushed up to:代表flush链表中被最早修改的那个页面对应的oldest_modification属性值。(意味着已经写入脏页的lsn)
Last checkpoint at:当前系统的checkpoint_lsn值
九 Innodb_flush_log_at_trx_commit
0:当该系统变量值为0时,表示在事务提交时不立即向磁盘中同步redo日志,这个任务是交给后台线程做的。
1:当该系统变量值为1时,表示在事务提交时需要将redo日志同步到磁盘,可以保证事务的持久性。1也是innodb_flush_log_at_trx_commit的默认值。
2:当该系统变量值为2时,表示在事务提交时需要将redo日志写到操作系统的缓冲区中,但并不需要保证将日志真正的刷新到磁盘
十 崩溃恢复
1 确定恢复的起点
checkpoint_lsn之前的redo日志都可以被覆盖,它们已经被刷盘,我们就没必要恢复它们了。
checkpoint_lsn之后的redo日志,它们对应的脏页可能没被刷盘,也可能被刷盘了,不能确定,所以需要从checkpoint_lsn开始读取redo日志来恢复页面
checkpoint1和checkpoint2这两个block中的checkpoint_no值读出来比一下大小,哪个的checkpoint_no值更大,说明哪个block存储的就是最近的一次checkpoint信息。
这样就能拿到最近发生的checkpoint对应的checkpoint_lsn值以及它在redo日志文件组中的偏移量checkpoint_offset
2 确定恢复的终点
我们说在写redo日志的时候都是顺序写的,写满了一个block之后会再往下一个block中写:
普通block的log block header部分有一个称之为LOG_BLOCK_HDR_DATA_LEN的属性,该属性值记录了当前block里使用了多少字节的空间
对于被填满的block来说,该值永远为512。如果该属性的值不为512,那么就是它了,它就是此次奔溃恢复中需要扫描的最后一个block
3 恢复过程
1) redo0在checkpoint_lsn后边,我按照redo日志的顺序依次扫描checkpoint_lsn之后的各条redo日志,按照日志中记载的内容将对应的页面恢复出来
2) 使用哈希表
redo日志的space ID和page number属性计算出散列值,把space ID和page number相同的redo日志放到哈希表的同一个槽里,如果有多个space ID和page number都相同的redo日志,那么它们之间使用链表连接起来,按照生成的先后顺序链接起来的
遍历哈希表,因为对同一个页面进行修改的redo日志都放在了一个槽里,所以可以一次性将一个页面修复好(避免了很多读取页面的随机IO),这样可以加快恢复速度
3) 跳过已经刷新到磁盘的页面
但是checkpoint_lsn之后的redo日志我们不能确定是否已经刷到磁盘,主要是因为在最近做的一次checkpoint后,可能后台线程又不断的从LRU链表和flush链表中将一些脏页刷出Buffer Pool。这
那在恢复时怎么知道某个redo日志对应的脏页是否在奔溃发生时已经刷新到磁盘了呢?
这还得从页面的结构说起,我们前边说过每个页面都有一个称之为File Header的部分,
在File Header里有一个称之为FIL_PAGE_LSN的属性,该属性记载了最近一次修改页面时对应的lsn值(其实就是页面控制块中的newest_modification值)。
如果在做了某次checkpoint之后有脏页被刷新到磁盘中,那么该页对应的FIL_PAGE_LSN代表的lsn值肯定大于checkpoint_lsn的值,凡是符合这种情况的页面就不需要重复执行checkpoint_lsn值小于FIL_PAGE_LSN的redo日志了,所以更进一步提升了奔溃恢复的速度。
十一 LOG_BLOCK_HDR_NO的计算
实际存储redo
日志的普通的log block
来说,在log block header
处有一个称之为LOG_BLOCK_HDR_NO
的属性
这个属性代表一个唯一的标号。这个属性是初次使用该block时分配的,跟当时的系统lsn
值有关。使用下边的公式计算该block的LOG_BLOCK_HDR_NO
值:
((lsn / 512) & 0x3FFFFFFFUL) + 1
LOG_BLOCK_HDR_NO值的第一个比特位比较特殊,称之为flush bit,如果该值为1,代表着本block是在某次将log buffer中的block刷新到磁盘的操作中的第一个被刷入的block
- 点赞
- 收藏
- 关注作者
评论(0)