[mysql]innodb的redo log

目录

前言

常见问题

redo log

简单的redo log的结构

什么样的操作会直接简化到这种程度,直接记录一下物理日志就行呢?

复杂的的redo log的结构

啥时候会有这么复杂的结构呢?物理+逻辑一起记录redo log?

redo日志既包含了物理层,也包含了逻辑层?

怎么保证一系列的redo log具有原子性的?

redo log的写入优化?

redo日志缓冲区

redo日志写入log buffer?

redo日志刷盘时机

redo log是一组文件,可以理解为一个环

惊!mysql也有checkpoint

做checkpoint的步骤?

redo log刷写的过程是什么样?(使用链表缓存没有刷的脏页)

标记当前log buffer中已经有哪些日志被刷新到磁盘中了?

故障恢复过程

1.确定恢复的起点(从checkpoint的最后序号进行恢复页面)

2.确定恢复的终点(找到没有写满的文件,就是终点哦)

3. 恢复(使用hash表加快回复,跳过已经刷回到磁盘的页面)

总结


前言

今天被问到,之前也没太在意这个事情。

今天被问到就好好刷一遍。

强烈推荐MySQL 是怎样运行的:从根儿上理解 MySQL

知识付费的时间,学好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_1BYTEtype字段对应的十进制数字为1):表示在页面的某个偏移量处写入1个字节的redo日志类型。

  • MLOG_2BYTEtype字段对应的十进制数字为2):表示在页面的某个偏移量处写入2个字节的redo日志类型。

  • MLOG_4BYTEtype字段对应的十进制数字为4):表示在页面的某个偏移量处写入4个字节的redo日志类型。

  • MLOG_8BYTEtype字段对应的十进制数字为8):表示在页面的某个偏移量处写入8个字节的redo日志类型。

  • MLOG_WRITE_STRINGtype字段对应的十进制数字为30):表示在页面的某个偏移量处写入一串数据。

一般都不会这么少的内容的,我们都需要知道:

复杂的的redo log的结构

啥时候会有这么复杂的结构呢?物理+逻辑一起记录redo log?

1.执行一条语句会修改非常多的页面,包括系统数据页面和用户数据页面(用户数据指的就是聚簇索引和二级索引对应的B+树)。

比如需要插入语句,还需要更新b+树:

  • 表中包含多少个索引,一条INSERT语句就可能更新多少棵B+树。

  • 针对某一棵B+树来说,既可能更新叶子节点页面,也可能更新内节点页面,也可能创建新的页面(在该记录插入的叶子节点的剩余空间比较少,不足以存放该记录时,会进行页面的分裂,在内节点页面中添加目录项记录)。

INSERT语句对所有页面的修改都得保存到redo日志中去。比方说将记录插入到聚簇索引中时,数据页中除了存储实际的记录之后,还有什么File HeaderPage HeaderPage 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_INSERTredo日志并没有记录PAGE_N_DIR_SLOTS的值修改为了啥,PAGE_HEAP_TOP的值修改为了啥,PAGE_N_HEAP的值修改为了啥等等这些信息,而只是把在本页面中插入一条记录所有必备的要素记了下来,之后系统崩溃重启时,服务器会调用相关向某个页面插入一条记录的那个函数,而redo日志中的那些数据就可以被当成是调用这个函数所需的参数,在调用完该函数后,页面中的PAGE_N_DIR_SLOTSPAGE_HEAP_TOPPAGE_N_HEAP等等的值也就都被恢复到系统崩溃前的样子了。这就是所谓的逻辑日志的意思。

redo日志既包含了物理层,也包含了逻辑层?

  • 物理层面看,这些日志都指明了对哪个表空间的哪个页进行了修改。

  • 逻辑层面看,在系统崩溃重启时,并不能直接根据这些日志里的记载,将页面内的某个偏移量处恢复成某个数据,而是需要调用一些事先准备好的函数,执行完这些函数后才可以将页面恢复成系统崩溃前的样子。

怎么保证一系列的redo log具有原子性的?

(mysql 的原子性保证中,也必然需要有这个东西的存在!)

规定在执行这些需要保证原子性的操作时必须以的形式来记录的redo日志,在进行系统崩溃重启恢复时,针对某个组中的redo日志,要么把全部的日志都恢复掉,要么一条也不恢复。

如何把这些redo日志划分到一个组里边儿呢?就是在该组中的最后一条redo日志后边加上一条特殊类型的redo日志,该类型名称为MLOG_MULTI_REC_ENDtype字段对应的十进制数字为31,该类型的redo日志结构很简单,只有一个type字段:

太优秀了

所以某个需要保证原子性的操作产生的一系列redo日志必须要以一个类型为MLOG_MULTI_REC_END结尾,就像这样:

这样在系统崩溃重启进行恢复时,只有当解析到类型为MLOG_MULTI_REC_ENDredo日志,才认为解析到了一组完整的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_logfile0ib_logfile1的文件,log buffer中的日志默认情况下就是刷新到这两个磁盘文件中。

磁盘上的redo日志文件不只一个,而是以一个日志文件组的形式出现的。这些文件以ib_logfile[数字]数字可以是012...)的形式进行命名。在将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_NO8服务器做checkpoint的编号,每做一次checkpoint,该值就加1。
LOG_CHECKPOINT_LSN8服务器做checkpoint结束时对应的LSN值,系统崩溃恢复时将从该值开始。
LOG_CHECKPOINT_OFFSET8上个属性中的LSN值在redo日志文件组中的偏移量
LOG_CHECKPOINT_LOG_BUF_SIZE8服务器在做checkpoint操作时对应的log buffer的大小
LOG_BLOCK_CHECKSUM4本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_lsnredo日志文件组中对应的偏移量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,我们只要把checkpoint1checkpoint2这两个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 IDpage number属性计算出散列值,把space IDpage number相同的redo日志放到哈希表的同一个槽里,如果有多个space IDpage 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_SLOTSPAGE_HEAP_TOPPAGE_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的值,说明已经被刷到磁盘了)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值