Session 20 redo日志(上)

在介绍事务的时候又强调过一个称之为持久性的特性,就是说对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个事务对数据库中所做的更改也不能丢失。

假设突然发生了某个故障,导致内存中的还没刷入的数据都失效了,那么这个已经提交了的事务对数据库中所做的更改也就跟着丢失了。那么如何保证这个持久性呢?一个很简单的做法就是【在事务提交的同时把该事务所修改的所有页面都刷新到磁盘】。

但是这个简单粗暴的做法有些问题。有时候我们仅仅修改了某个页面中的一个字节,这样刷新一个完整的数据页太浪费了;有时一个事务可能包含很多语句,即使是一条语句也可能修改许多页面,如果这些页面不相邻,那么随机IO刷起来比较慢。

再次回到我们的初心:我们只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来。所以我们其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把修改了哪些东西记录一下就好

redo日志格式

作为了解,我们没必要把InnoDB中的各种类型的redo日志格式都研究的透透的,目的是为了明白:redo日志会把事务在执行过程中对数据库所做的所有修改都记录下来,在之后系统奔溃重启后可以把事务所做的任何修改都恢复出来

redo日志本质上只是记录了一下事务对数据库做了哪些修改。 绝大部分类型的redo日志都有下面这种通用的结构:

image-20230804105319180

  • type:该条redo日志的类型。

      在MySQL 5.7.21这个版本中,设计InnoDB的大佬一共为redo日志设计了53种不同的类型。

  • space ID:表空间ID。

  • page number:页号。

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

简单的redo日志类型

把整个页面看作字节序列,忽视原本的文件结构。

  • MLOG_1BYTEtype字段对应的十进制数字为1):表示在页面的某个偏移量处写入1个字节的redo日志类型。
  • MLOG_2BYTEtype字段对应的十进制数字为2):表示在页面的某个偏移量处写入2个字节的redo日志类型。
  • MLOG_4BYTEtype字段对应的十进制数字为4):表示在页面的某个偏移量处写入4个字节的redo日志类型。
  • MLOG_8BYTEtype字段对应的十进制数字为8):表示在页面的某个偏移量处写入8个字节的redo日志类型。
  • MLOG_WRITE_STRINGtype字段对应的十进制数字为30):表示在页面的某个偏移量处写入一串数据。

比如每当向某个包含隐藏的row_id列的表中插入一条记录时,需要到某个页中找到系统维护的Max Row ID(8字节)变量,为其+1。这时便可以用MLOG_8BYTE

复杂一些的redo日志类型

有时候把一条记录插入到一个页面时需要更改的地方非常多,以一条INSERT语句为例(在页面空间足够时),可能会涉及:

  • 可能更新Page Directory中的槽信息。
  • Page Header中的各种页面统计信息。
  • 数据页里的记录是按照索引列从小到大的顺序组成一个单向链表的,需要更新上一条记录的记录头信息中的next_record属性。
  • 还有别的等等的更新的地方

image-20230804114447703

因为把一条记录插入到一个页面时需要更改的地方非常多,因而不能简单地视作字节序列、用简单的redo日志。

设计InnoDB的大佬本着勤俭节约的初心,提出了一些新的redo日志类型,比如:

  • MLOG_REC_INSERT(对应的十进制数字为9):表示插入一条使用非紧凑行格式的记录时的redo日志类型。

  • MLOG_COMP_REC_INSERT(对应的十进制数字为38):表示插入一条使用紧凑行格式的记录时的redo日志类型。

  • MLOG_COMP_PAGE_CREATEtype字段对应的十进制数字为58):表示创建一个存储紧凑行格式记录的页面的redo日志类型。

  • MLOG_COMP_REC_DELETEtype字段对应的十进制数字为42):表示删除一条使用紧凑行格式记录的redo日志类型。

  • MLOG_COMP_LIST_START_DELETEtype字段对应的十进制数字为44):表示从某条给定记录开始删除页面中的一系列使用紧凑行格式记录的redo日志类型。

  • MLOG_COMP_LIST_END_DELETEtype字段对应的十进制数字为43):与MLOG_COMP_LIST_START_DELETE类型的redo日志呼应,表示删除一系列记录直到MLOG_COMP_LIST_END_DELETE类型的redo日志对应的记录为止。

这些类型的redo日志既包含物理层面的意思,也包含逻辑层面的意思,具体指:

  • 物理层面看,这些日志都指明了对哪个表空间的哪个页进行了修改。
  • 逻辑层面看,在系统奔溃重启时,并不能直接根据这些日志里的记载,将页面内的某个偏移量处恢复成某个数据,而是需要调用一些事先准备好的函数,执行完这些函数后才可以将页面恢复成系统奔溃前的样子(与简单redo日志的区别)。

直接看一下这个类型为MLOG_COMP_REC_INSERTredo日志的结构:

image-20230804114859519

  • 图中n_uniques的值的含义是在一条记录中,需要几个字段的值才能确保记录的唯一性。对于聚簇索引来说,n_uniques的值为主键的列数,对于其他二级索引来说,该值为索引列数+主键列数。这样当插入一条记录时就可以按照记录的前n_uniques个字段进行排序。

  • offset代表的是该记录的前一条记录在页面中的地址。为什么要记录前一条记录的地址呢?这是因为每向数据页插入一条记录,都需要修改该页面中维护的记录链表(包含在每条记录的记录头信息中)。

这个类型为MLOG_COMP_REC_INSERTredo日志,只是把在本页面中插入一条记录所有必备的要素记了下来,之后系统奔溃重启时,服务器会调用相关向某个页面插入一条记录的那个函数,而redo日志中的那些数据就可以被当成是调用这个函数所需的参数,在调用完该函数后,页面中的PAGE_N_DIR_SLOTSPAGE_HEAP_TOPPAGE_N_HEAP等等的值也就都被恢复到系统奔溃前的样子了。这就是所谓的逻辑日志的意思。

Mini-Transaction

以组的形式写入redo日志

我们以向某个索引对应的B+树插入一条记录为例,在向B+树中插入这条记录之前,需要先定位到这条记录应该被插入到哪个叶子节点代表的数据页中,定位到具体的数据页之后,有两种可能的情况:

  1. 数据页的剩余的空闲空间充足,足够容纳这一条待插入记录,那么事情很简单,直接把记录插入到这个数据页中,记录一条类型为MLOG_COMP_REC_INSERTredo日志就好了,我们把这种情况称之为==乐观插入==。

  2. 该数据页剩余的空闲空间不足,那么遇到这种情况要进行很多操作。

    • 可能更新Page Directory中的槽信息
    • Page Header中的各种页面统计信息
    • 页分裂(叶子或内节点)
    • 叶子页面双向链表
    • 在内节点中添加一条目录项记录指向这个新页
    • 其它

    很显然,这个过程要对多个页面进行修改,也就意味着会产生多条redo日志,我们把这种情况称之为==悲观插入==。

一次悲观插入就需要生成许多条redo日志。规定在执行这些需要保证原子性的操作时必须以====的形式来记录的redo日志,在进行系统奔溃重启恢复时,针对某个组中的redo日志,要么把全部的日志都恢复掉,要么一条也不恢复。(如何划分组,详见原文)。

Mini-Transaction的概念

设计MySQL的大佬把对底层页面中的一次原子访问的过程称之为一个Mini-Transaction,简称==mtr==,比如上面所说的修改一次Max Row ID的值算是一个Mini-Transaction,向某个索引对应的B+树中插入一条记录的过程也算是一个Mini-Transaction。通过上面的叙述我们也知道,一个所谓的mtr可以包含一组redo日志,在进行奔溃恢复时这一组redo日志作为一个不可分割的整体

一个事务可以包含若干条语句,每一条语句其实是由若干个mtr组成,每一个mtr又可以包含若干条redo日志,画个图表示它们的关系就是这样:

image-20230804115931343

redo日志的写入过程

设计InnoDB的大佬为了更好的进行系统奔溃恢复,他们把通过mtr生成的redo日志都放在了大小为512字节中。为了和表空间中的页做区别,这里把用来存储redo日志的页称为block。一个redo log block共512字节,示意图如下:

image-20230804120316241

  • LOG_BLOCK_HDR_NO:每一个block都有一个大于0的唯一标号,本属性就表示该标号值。
  • LOG_BLOCK_HDR_DATA_LEN表示block中已经使用了多少字节,初始值为12(因为log block body从第12个字节处开始)。随着往block中写入的redo日志越来也多,本属性值也跟着增长。如果log block body已经被全部写满,那么本属性的值被设置为512
  • LOG_BLOCK_FIRST_REC_GROUP:一条redo日志也可以称之为一条redo日志记录(redo log record),一个mtr会生产多条redo日志记录,这些redo日志记录被称之为一个redo日志记录组(redo log record group)。LOG_BLOCK_FIRST_REC_GROUP就代表该block中第一个mtr生成的redo日志记录组的偏移量(其实也就是这个block里第一个mtr生成的第一条redo日志的偏移量)。
  • LOG_BLOCK_CHECKPOINT_NO:表示所谓的checkpoint的序号。
  • LOG_BLOCK_CHECKSUM:表示block的校验值,用于正确性校验,我们暂时不关心它。

redo日志缓冲区

写入redo日志时也不能直接直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为redo log buffer的连续内存空间,翻译成中文就是redo日志缓冲区,我们也可以简称为log buffer。这片内存空间被划分成若干个连续的redo log block。可以通过启动参数innodb_log_buffer_size来指定log buffer的大小,在MySQL 5.7.21这个版本中,该启动参数的默认值为16MB

系统维护了一个称为buf_free的全局变量,该变量指明后续写入的redo日志应该写入到log buffer中的哪个位置。

image-20230804124017056

前面说过一个mtr执行过程中可能产生若干条redo日志,这些redo日志是一个不可分割的组,所以其实并不是每生成一条redo日志,就将其插入到log buffer中,而是每个mtr运行过程中产生的日志先暂时存到一个地方,当该mtr结束的时候,将过程中产生的一组redo日志再全部复制到log buffer。我们现在假设有两个名为T1T2的事务,每个事务都包含2个mtr,我们给这几个mtr命名一下:

  • 事务T1的两个mtr分别称为mtr_T1_1mtr_T1_2
  • 事务T2的两个mtr分别称为mtr_T2_1mtr_T2_2

image-20230804135416511

每个mtr都会产生一组redo日志,不同的事务可能是并发执行的,所以T1T2之间的mtr可能是交替执行的

image-20230804135331424

有的mtr产生的redo日志量非常大,比如mtr_t1_2产生的redo日志甚至占用了3个block来存储。