两次写可以说是在Innodb中很独特的一个功能点,而关于它的说明或者解释非常少,至于它存在的原因更没有多少文章来说,所以我打算专门对它做一次说明。

首先说明一下为什么会有两次写这个东西:
因为innodb中的日志是逻辑的,所谓逻辑就是比如当插入一条记录时,它可能会导致在某一个页面(这条记录最终被插入的位置)的多个偏移位置写入某个长度的值,比如页头的记录数,槽数,页尾槽数据,页中的记录值等等,这些本是一些物理操作,而innodb为了节约日志量及其它一些原因,设计为逻辑处理的方式,那就是它会在一个页面的基础上,把一条记录插入,那么在日志记录中记录的内容为表空间号、页面号、记录的各个列的值等等,在内部转换为上面的物理操作。

但这里的一个问题是,如果那个页面本身是错误的,这种错误有可能是因为写断裂(1个页面为16K,分多次写入,后面的有可能没有写成功,导致这个页面不完整)引起的,那么这个逻辑操作就没办法完成了,因为它的前提是这个页面还是正确的,完整的,因为如果这个页面不正确的话,这个页面里的数据是无效的,有可能产生各种不可预料的问题。

那么正是因为这个问题,所以必须要首先保证这个页面是正确的,方法就是两次写,它的思想最终是一种备份思想,也就是一种镜像。

下面就它的实现方式说明一下:

首先是它的创建,在初始化一个库的时候,系统会在系统页面5号页面的尾部(大约是16K-200字节的位置)初始化所有关于两次写的信息,这些信息包括:

----------------------------------------------------------------

#define TRX_SYS_DOUBLEWRITE_FSEG    0               /*这里存储的是两次写页面所在的段的地址信息 */
#define TRX_SYS_DOUBLEWRITE_MAGIC               FSEG_HEADER_SIZE
                                                                                                 /*!< 用来判断是不是已经初始化过两次写页面 */
#define TRX_SYS_DOUBLEWRITE_BLOCK1              (4 + FSEG_HEADER_SIZE)
                                                                                                 /*!两次写页面的第一个簇的首地址,两次写页面总共有2个簇,一个簇为64个页面*/
#define TRX_SYS_DOUBLEWRITE_BLOCK2              (8 + FSEG_HEADER_SIZE)
                                                                                                 /*!第二个簇的首地址*/
#define TRX_SYS_DOUBLEWRITE_REPEAT              12             /*!< 将上面的MAGCI、BLOCK1与BLOCK2重复存储,防止自己的不完整*/


----------------------------------------------------------------

两次写总共包括2M(默认值)的数据,有2个BLOCK,那么每一个BLOCK是1M,每个页面是16K,那么一个BLOCK包括64个页面,正是一个簇的大小。,所以其实两次写页面的空间是2个簇的空间。

那么初始化所要做的工作就是将上面的信息补充完整,BLOCK1与BLOCK2分别对应2个簇的首地址,同时还要申请2个簇的内存空间,用来缓存这些数据。

除上面的信息之外,还会有一个空间用来存储这128个页面的页面信息,是用来在刷两次写页面之后,要做对应的页面刷盘操作,这是一个长度为128的数组。

有了上面的信息之后,则两次写初始化完成。

下面说明一下它的使用过程:
在做页面刷盘的时候,如果开启了两次写的功能,则innodb要做的不是简单的直接将数据做io操作写入到硬盘,而是先将当前要写的页面按照顺序拷贝到两次写内存缓存空间中去,上面已经说了它的大小为128个页面的大小,同时要在页面数组中对应的将页面的地址记录下来,然后就算刷盘完成了,但实际上,此时要写出的页面都在两次写的内存缓存空间中的。

当缓存空间满了的时候,上面的操作会触发真正的刷盘操作,两次写的写入是,首先判断当前缓存中有多少个簇,也就是说判断BLOCK1中有没有数据,如果没有数据则直接不写了,如果有则写入,以这个簇实际大小写入,然后再判断BLOCK2中有没有数据,然后做同样的处理。
在写完两次写缓存中的数据之后,然后再将页面数组中的每一个页面按照顺序从前到后再一个一个的将其刷入到文件中,此时,才真正的将这些页面刷盘完成。

当然两次写缓存写出硬盘不只是上面一个机会,其它刷盘的操作也会触发这个操作,那时可能缓存中的页面数还没有达到128个。

上面已经说完了两次写的写入及初始化,最后说一下它是如何起作用的:

在数据库启动时(异常关闭的情况下),都会做数据库恢复(redo)操作,恢复的过程中,数据库都会检查页面是不是合法(校验等等),如果发现一个页面校验结果不一致,则此时会用到两次写这个功能,这个特点也正是为了处理这样的错误而设计的。

此时的操作很明白了,将两次写的2个BLOCK(簇)都读出来,然后将所有这些页面写回到对应的页面中去,那么这时可以保证这些页面是正确的,并且是在写入前已经更新过的(最新数据)。
在写回对应页面中去之后,那么就可以在这基础上继续做数据库恢复了,之后则不会再遇到这样的问题了,因为已经将最后有可能产生写断裂的数据页面都恢复了。

问题:
上面说的都是数据页面有问题的情况下可以通过两次写页面来恢复,但是如果2次写页面本身发生写断裂怎么办?
对于这个问题,其实是不用担心的,因为如果两次写有问题,则本身数据页面就没有做写操作,此时系统挂了,发生错误的是两次写页面,而数据页面在挂之前都是在buffer里面,文件中还是当前事务操作前的值,它自己没有变,还是一致状态,所以两次写页面压根就不会被使用到。

总结:

1. 两次写在任何时候记录的都是数据库最后发生改变的若干页面(最多128个),在数据库不断工作的过程中,它会不断的被覆盖,它始终是最新的数据,记录的是修改之后的页面数据,而不是修改之前的数据,它的作用不是还原数据,而是保证不会丢失修改。
2. 至于性能问题,表面看上去,它是每一个页面都写了2遍,则会非常影响性能,但实际上,由于将所写的页面都先缓存到内存中,到达128个之后才真正写入,那么对于磁盘而言,连续写与分散写(每个页面自己写)的性能相差很大的,而两次写正是将一个簇数量的页面组合起来形成2个连续的空间写入到两次写空间中,有效的利用这了这特点,所以性能是不会相差1倍的。实际上经过测试,可能两次写使得性能降低了10%。
3. 有其它一些数据库完全没有类似2次写的问题,比如达梦等,这个主要是由于它们采用了全物理的REDO,将一个页面的写操作都拆成一个个的小的物理写入,这种情况下就不会存在写断裂的情况,因为不管怎么写,日志都是对一个页面操作的重演,在REDO做完之后,页面的状态肯定是正确的。