Mysql工作原理——redo日志概念及类别

redo日志

InnoDB存储引擎是以页为单位来管理存储的,并且在真正访问页面之前,需要把磁盘上的页缓存到内存中的BufferPool之后才可以访问,但是数据库事务又强调持久性,就是对于一个已经提交的事务,在事务提交后即使系统发生崩溃,这个事务对数据库中所做的更改也不能丢失。

假设在事务提交后突然发生了某个故障,导致内存中的数据都失效了,但是由于我们只在内存Buffer Pool中修改了页面,所以导致数据丢失。解决这个问题最简单的方法就是在事务完成之前把该事务的所有页面都刷新到磁盘

但是这种做法有两个问题:

  1. 刷新一个完整的数据页太浪费了,有时候仅仅修改一个字节,而不得不将一个完成的数据页(16k)从内存中刷新到磁盘;
  2. 随机IO刷起来比较慢,一个事物可能包含多条语句,涉及多个页面的修改,而这些页面不相邻的话就会进行很多随机IO。

我们需要解决的主要问题是让已经提交了的事务对数据库中的数据做永久修改,即使系统崩溃,再重启后也能把这种修改恢复出来,所以我们只需要把修改了那些东西记录一下就好了。比如某个事物将系统表空间中的第100个页面中偏移量为1000处的那个字节的值1改为2,我们只需记录一下这个操作,即使系统崩溃,恢复的时候只要按照上述内容执行这个操作就可以了。上述内容被称为重做日志,即redo日志

redo日志与整页面刷新相比的好处是:

  1. redo日志占用空间非常小;
  2. redo日志是顺序写入磁盘的。

redo日志的格式

redo日志的本质上只是记录一下事务对数据库做了哪些修改。

在这里插入图片描述

  • type:该条日志的类型;
  • space ID:表空间ID;
  • page number:页号;
  • data:该条redo日志的具体内容。

简单的redo日志类型

对于简单地修改,redo日志中只需要记录一下在某个页面的某个偏移量出修改了几个字节的值,具体被修改的内容是啥就好了。这种日志被称为物理日志,并且根据在页面中写入数据的多少划分为几种不同的redo日志类型。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

MLOG_WRITE_STRING类型的redo日志表示写入一串数据,但是因为不能确定写入的具体数据占用多少字节,所以需要在日志结构中添加一个len字段。

复杂一些的redo日志类型

有时候一条语句会修改非常多的页面,包括系统数据页和用户数据页。

对于用户来说,平时更关心的是语句对B+树所做的更新:

  1. 表中包含多个索引,一条insert语句就可能更新多少棵B+树;
  2. 针对一棵B+树,既可能更新叶子节点页面,也可能更新内节点页面,也可能创建新的页面。

如果定位到的叶子节点的剩余空间足够存储该记录时,那么只更新该叶子节点页面就好了,只记录一条MLOG_WRITE_STRING类型的redo日志,但是一个数据也中除了存储实际的记录之后,还有什么File Header、Page Header等部分,所以每往叶子节点代表的数据页插入一条记录时,还有其他很多地方跟着更新。

  • 可能更新Page Directory 中的槽信息;
  • Page Header中的各种页面统计信息;
  • 更新上一条记录的记录头信息中的next_record属性来维护这个单向链表等;

在这里插入图片描述
把一条记录插入到一个页面时需要更改很多地方,两种解决方案:

  • 方案一:在每个修改的地方都记录一条redo日志;

如图有多少个加粗的块,就写多少条物理redo日志,这样记录的缺点是修改的地方太多了,可能记录的redo日志占用空间都比整个页面占用的空间多;

  • 方案二:将整个页面的第一个被修改的字节到最后一个修改的字节之间所有的数据当成是一套物理redo日志的具体数据,从图中看出,在第一个修改的字节到最后一个修改的字节之间仍有许多未修改过的数据,如果全部加入redo日志岂不是太浪费了。

方案改进
在这里插入图片描述
在这里插入图片描述

  • 在一个数据页里不论是叶子节点还是非叶子节点,记录都是按照索引列从小到大的顺序排列的,对于二级索引,当索引列的值相同时,记录还需要按照主键值进行排序,图中n_unique的值含义是在一条记录中,需要几个字段的值才能确保记录的唯一性,这样当插入一条记录时就可以按照记录的前n_uniques个字段进行排序。对于聚簇索引来说,n_unques的值为主键的列数,对于其他二级索引来说,该值为索引列数+主键列数,特别注意对于唯一索引,由于唯一索引的值可能是null,该值仍然是索引列数+主键列数。
  • field1_len~fieldn_len代表着该记录若干个字段占用存储空间的大小,需要注意的是,这里不管该字段的类型是固定长度大小的,还是可变长度大小的,该字段占用的大小始终要写入redo日志。
  • offset代表的是该记录的前一条记录在页面中的地址,记录该信息的原因是每向数据页插入一条记录,都需要修改该页面中维护的记录链表。每条记录的记录头信息汇总都包含一个称为next_record的属性,所以在插入新纪录时,需要修改前一条记录的next_record属性。
  • 一条记录其实是由额外信息和真实数据这两部分组成的,这两部分的总大小就是一条记录占用存储空间的总大小,通过end_seg_len的值可以间接的计算出一条记录占用存储空间的总大小,为什么不直接存储一条记录的占用存储空间的总大小呢?这时因为写redo日志是一个非常频繁的操作,为了减小redo日志本身占用的存储空间大小,所以想出这些弯弯绕绕的算法来实现这个目标,end_seg_len这个字段就是为了节省redo日志存储空间而提出来的。
  • mismatch_index的值也是为了节省redo日志的大小而设立的。

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

redo日志小结

redo日志会把事务在执行过程中对数据库所做的所有修改都记录下来,在之后系统崩溃重启后可以把事务所做的任何修改都恢复出来。

Mini-Transaction

以组的形式写入redo日志

语句在执行过程中可能修改若干个页面,还会更新聚簇索引和二级索引对应的B+树中的页面,由于对这些页面的更改都发生在Buffer Pool中,所以在修改完页面之后需要记录一下相应的redo日志,在执行语句过程中产生的redo日志人为的划分成了若干个不可分割的组:

  • 更新Max Row ID属性时产生的redo日志是不可分割的;
  • 向聚簇索引或二级索引对应B+树的页面中插入一条记录时产生的redo日志是不可分割的;
  • 还有其他一些对页面的访问操作产生的redo日志是不可分割的。

什么叫做不可分割?

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

  • 该数据页的剩余空间充足,足够容纳这条待插入记录,那么直接把记录插入这个数据页,记录一条类型为MLOG_COMP_REC_INSERT的日志就好了,这种情况被称为乐观插入

在这里插入图片描述
在这里插入图片描述

  • 情况二:该数据页剩余空闲空间不足,遇到这种情况要进行页分裂操作,就是新建一个叶子节点,然后把原先数据页中的一部分记录复制到这个新的数据页中,然后把记录插入进去,把这个叶子节点插入到叶子节点链表中,最后还要在内节点中添加一条目录记录指向这个新创建的页面。这个过程要对多个页面进行修改,也就意味着会产生多条redo日志,这种情况被称为悲观插入

在这里插入图片描述
在这里插入图片描述

如果作为内节点的页a的剩余空闲空间也不足以容纳增加一条目录项记录,那需要继续做内节点页a的分裂操作,也就意味着会修改更多的页面,从而产生更多 的redo日志。另外,对于悲观插入来说,由于需要新申请数据页,还需要改动一些系统页面,比方说要修改各种段、区的统计信息信息,各种链表的统计信息 (比如什么FREE链表、FSP_FREE_FRAG链表吧啦吧啦我们在唠叨表空间那一章中介绍过的各种东东)等等等等,反正总共需要记录的redo日志有二、三十条。

向某个索引对应的B+树中插入一条记录的这个过程必须是原子的,否则会形成一棵不正确的B+树,所以规定在执行这些需要保证原子性的操作时必须以组的形式来记录redo日志,在恢复的时候针对某个组的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日志。

  • 有的需要保证原子性的操作只生成一条redo日志。
    redo日志类型比较多,但是小于127,所以用7个比特位就足以包括所有的redo日志类型,而type字段其实是占用1个字节的。

在这里插入图片描述
如果type第一个比特位为1,代表该需要保证原子性的操作只产生了单一的一条redo日志,否则表示该原子性操作产生一系列的redo日志。

Mini-Transaction的概念

把对底层页面中的一次原子访问的过程称为一个Mini-Transaction,简称为mtr,如向某个索引对应的B+树插入一条记录的过程算是一个,一个mtr包含一组redo日志,在进行恢复的时候这一组redo日志作为一个不可分割的整体。

一个事物包含若干条语句,每一条语句包含若干mtr,每一个mtr又包含若干redo日志。

在这里插入图片描述

redo日志的写入过程

redo log block

为了更好地进行系统崩溃恢复,把通过mtr生成的redo日志都放在了大小为512字节的页中,这里把存储redo日志的页称为block。

在这里插入图片描述
真正的redo日志存储到占用496字节大小的log block body中。log block header和log block trailer存储的是一些管理信息。

在这里插入图片描述

redo日志缓冲区

为了解决磁盘速度过慢的问题而引入Buffer Pool,同理,写入redo日志时也不能直接写到磁盘上,实际上在服务器启动时就向操作系统申请一大片称之为redo log buffer 的连续内存空间,即redo日志缓冲区,简称为log buffer,这片内存空间被划分为若干个连续的redo log block。默认大小为16MB。

在这里插入图片描述

redo日志写入log buffer

向log buffer中写入redo日志的过程式顺序的,写入时,第一个问题就是应该写在哪个block的偏移量处,所以设计了一个称为bug_free的全局变量,该变量指明后续写入的redo日志应该写入到log buffer中的那个位置。

在这里插入图片描述
一个mtr执行过程中可能产生若干条redo日志,这些redo日志是一个不可分割的组,所以并不是每生成一条redo日志,就将其插入到log buffer中,而是每个mtr运行过程中产生的日志先暂存到一个地方,当mtr结束的时候,将过程中产生的一组redo日志再全部复制到log buffer中。
在这里插入图片描述

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值