MySQL中的 redolog
什么是redo log
如果我们只在内存的 Bufer Pool中修改了页面,假设在事务提交后突然发生了某个故障导致内存中的数据都失效了,那么这个已经提交的事务在数据库中所做的更改也就跟着丢失了,这是我们所不能忍受的。那么,如何保证这个持久性呢?一个很简单的做法就是在事务提交完成之前,把该事务修改的所有页面都刷新到磁盘。不过这个简单粗暴的做法存在下面这些问题。
刷新一个完整的数据页太浪费了。有时我们仅仅修改了某个页面中的一个字节,但是由于InnoDB 是以页为单位来进行磁盘I/O的,也就是说在该事务提交时不得不将一个完整的页面从内存中刷新到磁盘。我们又知道,一个页面的默认大小是16KB,因为修改了一个字节就要刷新 16KB的数据到磁盘上,显然太浪费了。随机 I/O 刷新起来比较慢。
一个事务可能包含很多语句,即使是一条语句也可能修改许多页面,“倒霉催”的是该事务修改的这些页面可能并不相邻。这就意味着在将某个事务修改的 Bufer Pool中的页面刷新到磁盘时,需要进行很多的随机 I/O。随机 I/O 比顺序 I/O 要慢,尤其是对于传统的机械硬盘。
所以,其实没有必在每次提交事务时就把该事务在内存中修改过的页面全部刷新到磁盘,只需要记录有哪些就该就可以。
这样在事务提交时,就会把上述内容刷新到磁盘中。即使之后系统崩溃了,重启之后只要照上述内容所记录的步骤重新更新一下数据页,那么该事务对数库中所体的修改可以恢复出来,这样也就意味着满足持久性的要求。
我们把这类日志叫做redo log日志。
redo 日志占用的空间非常小,在存储表空间ID、页号、偏移量以及需要更新的值时需要的存储空间很小。
redo 日志是顺序写入磁盘的,在执行事务的过程中,每执行一条语句,就可能产生若干条 redo 日志,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序 I/O。
redolog日志格式
redo 日志中各个部分的详细解释如下:
type :这条 redo 日志的类型。
page number:页号。
space ID:表空间 ID。
data :这条 redo 日志的具体内容。
简单的redolog日志
如果没有为某个表显式地定义主键,并且表中也没有定义不允许存储 NULL值的 UNIQUE键,那么InnoDB 会自动为表添加一个名为row_id的隐藏列作为主键。为这个row id 隐藏列进行赋值的方式如下。服务器会在内存中维护一个全局变量,每当向某个包含row_id隐藏列的表中插入一条记录时,就会把这个全局变量的值当作新记录的row_id列的值,并且把这个全局变量自增1。
每当这个全局变量的值为256的倍数时,就会将该变量的值刷新到系统表空间页号为7的页面中一个名为MaxRowID的属性中(前文介绍表空间结构时详细说过该属性。之所以不是每次自增该全局变量时就将该值刷新到磁盘,是为了避免频刷盘)
当系统启动时,会将这个MaxRowD属性加载到内存中,并将该值加上 256之后赋
值给前面提到的全局变量(因为在系统上次关机时,该全局变量的值可能大于磁盘页面中 Max Row ID 属性的值)。
这个 Max RowID 属性占用的存储空间是8字节。当某个事务向某个包含 row_id隐藏列的表插入一条记录,并且为该记录分配的row_id值为256的倍数时,就会向系统表空间页号头7的页面的相应偏移量处写入8字节的值。但是我们要知道,这个写入操作实际上是在Bufe Pool中完成的,我们需要把这次对这个页面的修改以redolog日志的形式记录下来。这样在事务提交之后,即使系统崩溃了,也可以将该页面恢复成崩溃前的状态。在这种对页画的修改是极其简单的情况下,redo 日志中只需要记录一下在某个页面的某个偏移量处修改了几个字节的值、具体修改后的内容是啥就好了。这种极其简单的redo 日志称为物理日志,并且根据在页面中写入数据的多少划分了几种不同的redo 日志类型。
复杂一些的 redo 日志类型
有时,在执行一条语句时会修改非常多的页面,包括系统数据页面和用户数据页面(用户数据指的就是聚簇索引和二级索引对应的 B+树)。以一条INSERT 语句为例,它除了向 B+树的页面中插入数据外,也可能更新系统数据MaxRowID的值。不过对于用户来说,平时更关心的是语句对B+树所做的更新。
表中包含多少个索引,一条INSERT 语句就可能更新多少棵 B+树。针对某一棵B+树来说,既可能更新叶子节点页面,也可能更新内节点页面,还可能创建新的页面(在该记录插入的叶子节点的剩余空间比较少,不足以存放该记录时,会进行页面分裂,在内节点页面中添加目录项记录)。
在语句执行过程中,INSERT语句对所有页面的修改都得保存到redo log日志中去。这句话说的比较轻巧,做起来可就比较麻烦了。比如,在将记录插入到聚簇索引中时,如果定位到的叶子节点的剩余空间足够存储该记录,那么只更新该叶子节点页面,并只记录一条 MLOGWRITE STRING类型的redo日志,表明在页面的某个偏移量处增加了哪些数据不就好了么?那就天真了。别忘了,一个数据页中除了存储实际的记录之外,还有FileHeader、PageHeader、Page Directory等部分(在第5章有详细讲解)。所以每往叶子节点代表的数据页中插入一条记录,还有其他很多地方会跟着更新,比如:
可能更新 Page Directory 中的槽信息;
可能更新 Page Header 中的各种页面统计信息,比如 PAGE_N_DIR_SLOTS 表示的槽数量可能会更改,PAGEHEAPTOP代表的还未使用的空间最小地址可能会更改,PAGENHEAP代表的本页面中的记录数量可能会更改……
说了这么多,就是想表达:在把一条记录插入到一个页面时,需要更改的地方非常多。这时如果使用前面介绍的简单的物理redo 日志来记录这些修改,可以有两种解决万案。
方案1:在每个修改的地方都记录一条redo日志。按照这种方式来记录redo 日志的缺点是显而易见的,因为被修改的地方实在太多了,可能redo 日志占用的空间都要比整个页面占用的空间多。
方案 2:将整个页面第一个被修改的字节到最后一个被修改的字节之间所有的数据当成一条物理 redo 日志中的具体数据。
正是因为在使用上面这两个方案来记录某个页面中做了哪些修改时,比较浪费空间,设计ImnoDB 的大叔本着勤俭节约的初心,提出了一些新的redo 日志类型。
MLOG REC INSERT(type 字段对应的十进制数字为9):表示在插入一条使用非紧凑行格式(REDUNDANT)的记录时,redo日志的类型,MLOG COMP RECINSERT(type 字段对应的十进制数字为38):表示在插入一条使用紧凑行格式(COMPACT、DYNAMIC、COMPRESSED)的记录时,redo 日志的类型。MLOG COMP PAGE CREATE(type 字段对应的十进制数字为 58):表示在创建一个存储紧凑行格式记录的页面时,redo日志的类型。
MLOG_COMP REC DELETE(type 字段对应的十进制数字为 42):表示在删除一条使用紧凑行格式记录时,redo日志的类型。