redo日志探究
一、什么是redo日志
redo日志,也被称为重做日志,对数据库中表的操作进行记录,可以用于系统崩溃时的数据恢复。
例如,某个事务将系统表空间中的第100号页面中偏移量为1000处的那个字节的值1改为了2,我们只需要记录将第0号表空间的100号⻚⾯的偏移量为1000处的值更新为2,这样我们在事务提交时,把上述内容刷新到磁盘中,即使之后系统崩溃了,重启之后只要按照上述内容记录的步骤重新更新一下数据页,那么该事务对数据库中所做的修改又可以被恢复出来。
二、redo日志的优点
与在事务提交时将所有修改过的内存中的页面刷新到磁盘中相比,只将事务执行过程中产生的redo日志刷新到磁盘的好处如下:
- redo日志占用的空间很小
存储表空间ID、页号、偏移量以及需要更新的值所需要的存储空间是很小的。 - redo日志是顺序写入磁盘的
在执行事务的过程中,每执行一条语句,就可能产生若干条redo日志,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序IO。
三、redo日志格式
各个部分详细释义如下:
- type:该条redo日志的类型
- space ID:表空间
- page number:页号
- data:该条redo日志的具体内容
3.1 redo日志类型
- 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):表示在⻚⾯的某个偏移量处写⼊⼀串数据。
四、Mini-Transaction
4.1 以组的形式写入redo日志
一条语句在执行过程中可能修改若干个页面。这些页面的更改都发生在Buffer Pool中,所以在修改完页面之后,需要记录下相应日志的redo日志。在执行语句的过程中产生的redo日志在InnoDB中划分成了若干个不可分割的组,比如:
- 更新Max Row ID属性时产生的redo日志是不可分割的。
- 向聚簇索引对应的B+树的页面中插入一条记录时产生的redo日志是不可分割的。
- 向某个二级索引对应的B+树的页面中插入一条记录时产生的redo日志是不可分割的。
- 还有其他的一些对页面的访问操作时产生的redo日志也是不可分割的。
什么是不可分割?不可分割类似于事务,即要么都完成,要么都不完成。
我们以向某个索引对应的B+树插入一条记录为例,在向B+树中插入这条记录之前,需要先定位到这条记录应该被插入到哪个叶子节点代表的数据页中,定位到具体的数据页之后,有两种可能的情况:
- 情况一:数据页的剩余空间充足,足够容纳这一条待插入记录,这时,只要直接插入到这个数据页就行,记录一条类型为MLOG_COMP_REC_INSER的redo日志就好了,我们把这种情况称之为乐观插入。
- 情况二:该数据页剩余空间不足,这时需要进行页分裂操作,也就是新建一个叶子节点,然后把原先数据页中的一部分记录复制到这个新的数据页中,然后再把记录插入进去,再把这个叶子节点插入到叶子节点链表中,最后还要在内节点中添加一条目录项记录指向这个新创建的页面,很显然,这个过程要对多个页面进行修改,也就意味着会产生多条redo日志,我们把这种情况称之为悲观插入。
如果在悲观插入过程中,新的页面已经分配好了,数据也复制过去了,新的记录也插入到该页面中了,可是没有向内节点中插入一条目录项记录,这个插入就是不完整的,这样会形成一颗不正确的B+树。
InnoDB中规定,在执行这些保证原子性的操作时必须以组的形式来记录的redo日志,要么把全部的日志都恢复掉,要么一条都不恢复。如何做到这些的呢?
-
有的需要保证原子性的操作会生成多条redo日志,比如向某个索引对应的B+树中进行一次悲观插入就需要生成许多条redo日志。如何把这些redo日志划分到一个组内的呢?就是在组中的最后一条redo日志后边加上一条特殊类型的redo日志,该类型名称为MLOG_MULTI_REC_END,type字段对应的十进制数字是31,此时相当于加了一个结束标志。这样在系统崩溃重启进行恢复时,只有当解析到类型为MLOG_MULTI_REC_END的redo日志,才认为解析到了一组完整的redo日志,才会进行恢复,否则的话,直接放弃前边解析到的redo日志。
-
有时候需要保证原子性的操作只生成一条redo日志,比如更新Max Row ID属性的操作就只会生成一条redo日志。其实在一条日志后边跟一个类型为MLOG_MULTI_REC_END的redo日志也是可以的,不过为了节省空间,他们不想浪费⼀个⽐特位。 别忘了虽然redo⽇ 志的类型⽐较多, 但撑死了也就是⼏⼗种, 是⼩于127这个数字的, 也就是说我们⽤7个⽐特位就⾜以包括所有的redo⽇志类型,⽽type字段其实是占⽤1个字节的, 也就是说我们可以省出来⼀个⽐特位⽤来表示该需要保证原⼦性的操作只产⽣单⼀的⼀
条redo⽇志, 示意图如下:
4.2 Mini-Transaction的概念
顾名思义,Mini-Transaction就是微事务。把对底层页面中的一次原子访问的过程称之为一个Mini-Transaction,简称为mtr,一个所谓的mtr可以包含一组redo日志,在进行崩溃恢复时这一组redo日志作为一个不可分割的整体。
一个事务可以包含若干条语句,每一条语句其实是由若干个mtr组成,每一个mtr又可以包含若干条redo日志。
五、redo日志的写入过程
5.1 redo日志缓冲区
InnoDB为了解决磁盘速度过慢的问题而引入了Buffer Pool。同理,写入redo日志时也不能直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为redo log buffer的连续内存空间,翻译过来就是redo日志缓冲区,简称log buffer。
5.2 redo日志写入log buffer
向log buffer中写⼊redo⽇ 志的过程是顺序的, 也就是先往前边的block中写, 当该block的空闲空间⽤完之后再往下⼀个block中写。 当我们想往log buffer中写⼊redo⽇ 志时, 第⼀个遇到的问题就是应该写在哪个block的哪个偏移量处, InnoDB提供了⼀个称之为buf_free的全局变量, 该变量指明后续写⼊的redo⽇ 志应该写⼊到log buffer中的哪个位置。