目录
什么样的操作会直接简化到这种程度,直接记录一下物理日志就行呢?
啥时候会有这么复杂的结构呢?物理+逻辑一起记录redo log?
redo log刷写的过程是什么样?(使用链表缓存没有刷的脏页)
标记当前log buffer中已经有哪些日志被刷新到磁盘中了?
1.确定恢复的起点(从checkpoint的最后序号进行恢复页面)
3. 恢复(使用hash表加快回复,跳过已经刷回到磁盘的页面)
前言
今天被问到,之前也没太在意这个事情。
今天被问到就好好刷一遍。
知识付费的时间,学好mysql真不赖。
文章红色内容都是记忆点。
研究特别深入的存储这方面,也许不会问,但是蛮有趣。
注意redolog 其实记录的逻辑信息,重启之后,需要去做一个数据的回放,相当于调用方法什么的~恢复信息的。
常见问题
1. mysql是怎么实现原子性的?
2.redo log 和 binlog有什么区别?
那么先看一下redolog吧
<以下内容学习摘选自 《MySQL 是怎样运行的:从根儿上理解 MySQL》>
redo log
如果没有redolog,我们的痛点是什么:
在真正访问页面之前,需要把在磁盘上的页缓存到内存中的
Buffer Pool
之后才可以访问事务的更改也是要写到buffer pool,然后提交了才会刷回磁盘。
持久性:
就是说对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个事务对数据库中所做的更改也不能丢失。但是如果我们只在内存的Buffer Pool
中修改了页面,假设在事务提交后突然发生了某个故障,导致内存中的数据都失效了(所以不能让缓存的数据丢失对吧~)
因为mysql 都是以数据页取出并刷回磁盘的,一个数据页是16kb;
但是比如一个事务只是修改了一个字段,那么就要给它刷回磁盘,这个是不能忍得吧。
所以redo log诞生
系统崩溃了,重启之后只根据redolog 重新更新一下数据页,那么该事务对数据库中所做的修改又可以被恢复出来,也就意味着满足
持久性
的要求。因为在系统崩溃重启时需要按照上述内容所记录的步骤重新更新数据页,所以上述内容也被称之为
重做日志
,英文名为redo log
只将该事务执行过程中产生的
redo
日志刷新到磁盘的好处如下:
redo
日志占用的空间非常小
redo
日志是顺序写入磁盘的
简单的redo log的结构
-
type
:该条redo
日志的类型。 -
space ID
:表空间ID。 -
page number
:页号。 -
data
:该条redo
日志的具体内容。
其实我们一般就关心这个redolog 的data;
这种极其简单的redo
日志称之为物理日志;
什么样的操作会直接简化到这种程度,直接记录一下物理日志就行呢?
写入实际上是在
Buffer Pool
中完成的,我们需要为这个页面的修改记录一条redo
日志,以便在系统崩溃后能将已经提交的该事务对该页面所做的修改恢复出来。对页面的修改是极其简单的,redo
日志中只需要记录一下在某个页面的某个偏移量处修改了几个字节的值,具体被修改的内容是啥就好了我的个人简单理解就是,这个修改内容,只涉及到了对应某一处offset,修改一点点内容,写入一点点字节,小于8byte 或者一小串数据。
不涉及到索引的变更更改,还有什么变长乱七八糟的骚东西
-
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
):表示在页面的某个偏移量处写入一串数据。
一般都不会这么少的内容的,我们都需要知道:
复杂的的redo log的结构
啥时候会有这么复杂的结构呢?物理+逻辑一起记录redo log?
1.执行一条语句会修改非常多的页面,包括系统数据页面和用户数据页面(用户数据指的就是聚簇索引和二级索引对应的
B+
树)。比如需要插入语句,还需要更新b+树:
表中包含多少个索引,一条
INSERT
语句就可能更新多少棵B+
树。针对某一棵
B+
树来说,既可能更新叶子节点页面,也可能更新内节点页面,也可能创建新的页面(在该记录插入的叶子节点的剩余空间比较少,不足以存放该记录时,会进行页面的分裂,在内节点页面中添加目录项记录
)。
INSERT
语句对所有页面的修改都得保存到redo
日志中去。比方说将记录插入到聚簇索引中时,数据页中除了存储实际的记录之后,还有什么File Header
、Page Header
、Page Directory
等等部分,所以每往叶子节点代表的数据页里插入一条记录时,还有其他很多地方会跟着更新,比如说:
可能更新
Page Directory
中的槽信息。
Page Header
中的各种页面统计信息,比如PAGE_N_DIR_SLOTS
表示的槽数量可能会更改,PAGE_HEAP_TOP
代表的还未使用的空间最小地址可能会更改,PAGE_N_HEAP
代表的本页面中的记录数量可能会更改,吧啦吧啦,各种信息都可能会被修改。在数据页里的记录是按照索引列从小到大的顺序组成一个单向链表的,每插入一条记录,还需要更新上一条记录的记录头信息中的
next_record
属性来维护这个单向链表。总之就是有很多东西要更新,所以不能都记录下来,就混合记录物理的一些信息,然后加上逻辑信息。
在一个数据页里,不论是叶子节点还是非叶子节点,记录都是按照索引列从小到大的顺序排序的。对于二级索引来说,当索引列的值相同时,记录还需要按照主键值进行排序。
n_uniques
的值的含义是在一条记录中,需要几个字段的值才能确保记录的唯一性,这样当插入一条记录时就可以按照记录的前n_uniques
个字段进行排序。
- 对于聚簇索引来说,
n_uniques
的值为主键的列数,对于其他二级索引来说,该值为索引列数+主键列数。这里需要注意的是,唯一二级索引的值可能为NULL
,所以该值仍然为索引列数+主键列数。 -
field1_len ~ fieldn_len
代表着该记录若干个字段占用存储空间的大小,需要注意的是,这里不管该字段的类型是固定长度大小的(比如INT
),还是可变长度大小(比如VARCHAR(M)
)的,该字段占用的大小始终要写入redo
日志中。 -
offset
代表的是该记录的前一条记录在页面中的地址。为啥要记录前一条记录的地址呢?这是因为每向数据页插入一条记录,都需要修改该页面中维护的记录链表,每条记录的记录头信息
中都包含一个称为next_record
的属性,所以在插入新记录时,需要修改前一条记录的next_record
属性。 -
end_seg_len
的值可以计算出一条记录占用存储空间的总大小(end_seg_len
这个字段就是为了节省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
等等的值也就都被恢复到系统崩溃前的样子了。这就是所谓的逻辑
日志的意思。
redo
日志既包含了物理层,也包含了逻辑层?
-
物理层面看,这些日志都指明了对哪个表空间的哪个页进行了修改。
-
逻辑层面看,在系统崩溃重启时,并不能直接根据这些日志里的记载,将页面内的某个偏移量处恢复成某个数据,而是需要调用一些事先准备好的函数,执行完这些函数后才可以将页面恢复成系统崩溃前的样子。
怎么保证一系列的redo log具有原子性的?
(mysql 的原子性保证中,也必然需要有这个东西的存在!)
规定在执行这些需要保证原子性的操作时必须以组
的形式来记录的redo
日志,在进行系统崩溃重启恢复时,针对某个组中的redo
日志,要么把全部的日志都恢复掉,要么一条也不恢复。
如何把这些redo
日志划分到一个组里边儿呢?就是在该组中的最后一条redo
日志后边加上一条特殊类型的redo
日志,该类型名称为MLOG_MULTI_REC_END
,type
字段对应的十进制数字为31
,该类型的redo
日志结构很简单,只有一个type
字段:
太优秀了
所以某个需要保证原子性的操作产生的一系列redo
日志必须要以一个类型为MLOG_MULTI_REC_END
结尾,就像这样:
这样在系统崩溃重启进行恢复时,只有当解析到类型为MLOG_MULTI_REC_END
的redo
日志,才认为解析到了一组完整的redo
日志,才会进行恢复。否则的话直接放弃前边解析到的redo
日志。
(PS:只有一条的redolog 还有原子性的话,innodb可以用一个标记位来搞的:
如果type
字段的第一个比特位为1
,代表该需要保证原子性的操作只产生了单一的一条redo
日志,否则表示该需要保证原子性的操作产生了一系列的redo
日志。)
上述对底层页面中的一次原子访问的过程称之为一个Mini-Transaction
一个所谓的mtr(
Mini-Transaction
)
可以包含一组redo
日志,在进行崩溃恢复时这一组redo
日志作为一个不可分割的整体。
redo log的写入优化?
一般面试官可能不会问这么细的。这东西也不是做dba。但是如果有机会聊到 补充一下知识网络也是可以的。
redo日志缓冲区
同理插入的操作的Buffer Pool
。写入redo
日志时也不能直接直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为redo log buffer
的连续内存空间(log buffer)
。这片内存空间被划分成若干个连续的redo log block。
并不是每生成一条redo
日志,就将其插入到log buffer
中,而是每个mtr
运行过程中产生的日志先暂时存到一个地方,当该mtr
结束的时候,将过程中产生的一组redo
日志再全部复制到log buffer
中。
redo日志写入log buffer?
向
log buffer
中写入redo
日志的过程是顺序的,也就是先往前边的block中写,当该block的空闲空间用完之后再往下一个block中写。当我们想往log buffer
中写入redo
日志时,第一个遇到的问题就是应该写在哪个block
的哪个偏移量处,所以设计InnoDB
的大叔特意提供了一个称之为buf_free
的全局变量,该变量指明后续写入的redo
日志应该写入到log buffer
中的哪个位置.
redo日志刷盘时机
1. log buffer
空间不足时
2.事务提交时
我们前边说过之所以使用redo
日志主要是因为它占用的空间少,还是顺序写,在事务提交时可以不把修改过的Buffer Pool
页面刷新到磁盘,但是为了保证持久性,必须要把修改这些页面对应的redo
日志刷新到磁盘。
3.后台线程不停的刷刷刷(
-
后台有一个线程,大约每秒都会刷新一次
log buffer
中的redo
日志到磁盘。)
4.正常关闭服务器时
5.checkpoint
redo log是一组文件,可以理解为一个环
使用SHOW VARIABLES LIKE 'datadir'
查看)下默认有两个名为ib_logfile0
和ib_logfile1
的文件,log buffer
中的日志默认情况下就是刷新到这两个磁盘文件中。
磁盘上的redo
日志文件不只一个,而是以一个日志文件组
的形式出现的。这些文件以ib_logfile[数字]
(数字
可以是0
、1
、2
...)的形式进行命名。在将redo
日志写入日志文件组
时,是从ib_logfile0
开始写,如果ib_logfile0
写满了,就接着ib_logfile1
写,同理,ib_logfile1
写满了就去写ib_logfile2
,依此类推。如果写到最后一个文件该咋办?那就重新转到ib_logfile0
继续写。
(其实就是一个环,一组文件)
惊!mysql也有checkpoint
看到这我都惊了!我之前一直不知道原来mysql 也有checkpoint!
我以为只有kafka 和flink等大数据的 有这种概念。
但是为啥会有俩checkpoint?
规定,当
checkpoint_no
的值是偶数时,就写到checkpoint1
中,是奇数时,就写到checkpoint2
中。
各个属性的具体释义如下:
属性名 | 长度(单位:字节) | 描述 |
---|---|---|
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都有,我们不关心 |
因为我们redolog有限,所以会追上。
redo日志只是为了系统崩溃后恢复脏页用的,如果对应的脏页已经刷新到了磁盘,也就是说即使现在系统崩溃,那么在重启后也用不着使用redo日志恢复该页面了,所以该redo日志也就没有存在的必要了
那么它占用的磁盘空间就可以被后续的redo日志所重用。也就是说:判断某些redo日志占用的磁盘空间是否可以覆盖的依据就是它对应的脏页是否已经刷新到磁盘里。
做checkpoint的步骤?
比方说现在
页a
被刷新到了磁盘,redo
日志就可以被覆盖了,所以我们可以进行一个增加checkpoint_lsn
的操作,我们把这个过程称之为做一次checkpoint
。做一次
checkpoint
其实可以分为两个步骤:步骤一:计算一下当前系统中可以被覆盖的
redo
日志对应的lsn
值最大是多少。步骤二:将
checkpoint_lsn
和对应的redo
日志文件组偏移量以及此次checkpint
的编号写到日志文件的管理信息(就是checkpoint1
或者checkpoint2
)中。
PS:
-
设计者维护了一个目前系统做了多少次
checkpoint
的变量checkpoint_no
,每做一次checkpoint
,该变量的值就加1。我们前边说过计算一个lsn
值对应的redo
日志文件组偏移量是很容易的,所以可以计算得到该checkpoint_lsn
在redo
日志文件组中对应的偏移量checkpoint_offset
,然后把这三个值都写到redo
日志文件组的管理信息中。每一个
redo
日志文件都有2048
个字节的管理信息,但是上述关于checkpoint的信息只会被写到日志文件组的第一个日志文件的管理信息中。不过我们是存储到checkpoint1
中还是checkpoint2
中呢?规定,当checkpoint_no
的值是偶数时,就写到checkpoint1
中,是奇数时,就写到checkpoint2
中。
redo log刷写的过程是什么样?(使用链表缓存没有刷的脏页)
虽然有时候
redo
日志都已经被写到了磁盘上,但是它们修改的脏页仍然留在Buffer Pool
中,所以它们生成的redo
日志在磁盘上的空间是不可以被覆盖的。之后随着系统的运行,如果页a
被刷新到了磁盘,那么它对应的控制块就会从flush链表
中移除
简而言之,看看脏页有没有被刷到磁盘上面,如果有,就从flush链表移除
全局变量checkpoint_lsn
来代表当前系统中可以被覆盖的redo
日志总量是多少,这个变量初始值也是8704
。
redolog是有编号的
为记录已经写入的
redo
日志量,设计了一个称之为Log Sequence Number
的全局变量,简称lsn
。
每一组由mtr生成的redo日志都有一个唯一的LSN值与其对应,LSN值越小,说明redo日志产生的越早。
标记当前log buffer
中已经有哪些日志被刷新到磁盘中了?
redo
日志是首先写到log buffer
中,之后才会被刷新到磁盘上的redo
日志文件。所以设计InnoDB
的大叔提出了一个称之为buf_next_to_write
的全局变量,标记当前log buffer
中已经有哪些日志被刷新到磁盘中了。
故障恢复过程
虽然redo 在数据库没有挂的时候,显得很没用。
挂了的话就真有用了。
1.确定恢复的起点(从checkpoint的最后序号进行恢复页面)
checkpoint_lsn
之前的redo
日志都可以被覆盖,也就是说这些redo
日志对应的脏页都已经被刷新到磁盘中了,既然它们已经被刷盘,我们就没必要恢复它们了。对于checkpoint_lsn
之后的redo
日志,它们对应的脏页可能没被刷盘,也可能被刷盘了,我们不能确定,所以需要从checkpoint_lsn
开始读取redo
日志来恢复页面。
redo
日志文件组的管理信息中有两个block都存储了checkpoint_lsn
的信息,我们当然是要选取最近发生的那次checkpoint的信息。衡量checkpoint
发生时间早晚的信息就是所谓的checkpoint_no
,我们只要把checkpoint1
和checkpoint2
这两个block中的checkpoint_no
值读出来比一下大小,哪个的checkpoint_no
值更大,说明哪个block存储的就是最近的一次checkpoint
信息。这样我们就能拿到最近发生的checkpoint
对应的checkpoint_lsn
值以及它在redo
日志文件组中的偏移量checkpoint_offset
。
2.确定恢复的终点(找到没有写满的文件,就是终点哦)
redo
日志恢复的起点确定了,那终点是哪个呢?写redo
日志的时候都是顺序写的,写满了一个block之后会再往下一个block中写;
普通block的log block header
部分有一个称之为LOG_BLOCK_HDR_DATA_LEN
的属性,该属性值记录了当前block里使用了多少字节的空间。对于被填满的block来说,该值永远为512
。如果该属性的值不为512
,那么就是它了,它就是此次崩溃恢复中需要扫描的最后一个block。
3. 恢复(使用hash表加快回复,跳过已经刷回到磁盘的页面)
加快这个恢复的过程:
使用哈希表:
根据redo
日志的space ID
和page number
属性计算出散列值,把space ID
和page number
相同的redo
日志放到哈希表的同一个槽里,如果有多个space ID
和page number
都相同的redo
日志,那么它们之间使用链表连接起来,按照生成的先后顺序链接起来的
-
遍历哈希表,因为对同一个页面进行修改的
redo
日志都放在了一个槽里,所以可以一次性将一个页面修复好(避免了很多读取页面的随机IO),这样可以加快恢复速度。 -
按照时间顺序修复
-
跳过已经刷新到磁盘的页面(简而言之:其实就是根据checkpoint的lsn值与页面对应flie header中有一个页面的lsn值,如果页面的lsn值大于checkpoint的值,说明已经被刷到磁盘了)
总结
1. redo log 中有offset 是因为每向数据页插入一条记录,都需要修改该页面中维护的记录链表,每条记录的记录头信息
中都包含一个称为next_record
的属性,所以在插入新记录时,需要修改前一条记录的next_record
属性。
(这个很重要)
2.redo
日志并没有记录PAGE_N_DIR_SLOTS
的值修改为了啥,PAGE_HEAP_TOP
的值修改为了啥,PAGE_N_HEAP
的值修改为了啥等等这些信息,而只是把在本页面中插入一条记录所有必备的要素记了下来,之后系统崩溃重启时,服务器会调用相关向某个页面插入一条记录的那个函数,而redo
日志中的那些数据就可以被当成是调用这个函数所需的参数,在调用完该函数后,页面中的PAGE_N_DIR_SLOTS
、PAGE_HEAP_TOP
、PAGE_N_HEAP
等等的值也就都被恢复到系统崩溃前的样子了。这就是所谓的逻辑
日志的意思。
3. redo
日志既包含了物理层,也包含了逻辑层?
-
物理层面看,这些日志都指明了对哪个表空间的哪个页进行了修改。
-
逻辑层面看,在系统崩溃重启时,并不能直接根据这些日志里的记载,将页面内的某个偏移量处恢复成某个数据,而是需要调用一些事先准备好的函数,执行完这些函数后才可以将页面恢复成系统崩溃前的样子。
4.再谈一下页分裂- 针对某一棵B+
树来说,既可能更新叶子节点页面,也可能更新内节点页面,也可能创建新的页面(在该记录插入的叶子节点的剩余空间比较少,不足以存放该记录时,会进行页面的分裂,在内节点页面中添加目录项记录
)。
5.规定在执行这些需要保证原子性的操作时必须以组
的形式来记录的redo
日志,在进行系统崩溃重启恢复时,针对某个组中的redo
日志,要么把全部的日志都恢复掉,要么一条也不恢复。
6.写入redo
日志时也不能直接直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为redo log buffer
的连续内存空间(log buffer)
7.redo log恢复过程中,优化操作:
遍历哈希表,因为对同一个页面进行修改的redo
日志都放在了一个槽里,所以可以一次性将一个页面修复好(避免了很多读取页面的随机IO),这样可以加快恢复速度。
8.redo log恢复过程中,
跳过已经刷新到磁盘的页面(简而言之:其实就是根据checkpoint的lsn值与页面对应flie header中有一个页面的lsn值,如果页面的lsn值大于checkpoint的值,说明已经被刷到磁盘了)