日志分组
句在执行过程中可能修改若干个页面。比如我们前边说的一条 INSERT 语句可能修改系统表空间页号为 7 的页面的 Max Row ID 属性(当然也可能更新别的系统页面,只不过我们没有都列举出来而已),还会更新聚簇索引和二级索引对应 B+树中的页面。由于对这些页面的更改都发生在 Buffer Pool 中,所以在修改完页面之后,需要记录一下相应的 redo 日志。
在这个执行语句的过程中产生的redo 日志被InnoDB 人为的划分成了若干个不可分割的组,比如:
1、更新 Max Row ID 属性时产生的 redo 日志是不可分割的。
2、向聚簇索引对应 B+树的页面中插入一条记录时产生的 redo 日志是不可分割的。
3、向某个二级索引对应 B+树的页面中插入一条记录时产生的 redo 日志是不可分割的。
4、还有其他的一些对页面的访问操作时产生的 redo 日志是不可分割的….。
怎么理解这个不可分割的意思呢?我们以向某个索引对应的 B+树插入一条
记录为例,在向 B+树中插入这条记录之前,需要先定位到这条记录应该被插入
到哪个叶子节点代表的数据页中,定位到具体的数据页之后,有两种可能的情况: 情况一:该数据页的剩余的空闲空间充足,足够容纳这一条待插入记录,那
么事情很简单,直接把记录插入到这个数据页中,记录一条 redo 日志就好了,
我们把这种情况称之为乐观插入。
情况二:该数据页剩余的空闲空间不足,那么事情就很麻烦了,遇到这种情况要进行所谓的页分裂操作:
1、新建一个叶子节点;
2、然后把原先数据页中的一部分记录复制到这个新的数据页中;
3、然后再把记录插入进去,把这个叶子节点插入到叶子节点链表中;
4、非叶子节点中添加一条目录项记录指向这个新创建的页面;
5、非叶子节点空间不足,继续分裂。
很显然,这个过程要对多个页面进行修改,也就意味着会产生很多条 redo
日志,我们把这种情况称之为悲观插入。
另外,这个过程中,由于需要新申请数据页,还需要改动一些系统页面,比方说要修改各种段、区的统计信息信息,各种链表的统计信息,也会产生 redo 日志。
当然在乐观插入时也可能产生多条 redo 日志。
InnoDB **认为向某个索引对应的 B+树中插入一条记录的这个过程必须是原子**的,不能说插了一半之后就停止了。比方说在悲观插入过程中,新的页面已经分配好了,数据也复制过去了,新的记录也插入到页面中了,可是没有向非叶子节点中插入一条目录项记录,这个插入过程就是不完整的,这样会形成一棵不正确的 B+树。
我们知道 redo 日志是为了在系统崩溃重启时恢复崩溃前的状态,如果在悲观插入的过程中只记录了一部分 redo 日志,那么在系统崩溃重启时会将索引对应的 B+树恢复成一种不正确的状态。
所以规定在执行这些需要保证原子性的操作时必须以**组**的形式来记录的 redo 日志,在进行系统崩溃重启恢复时,针对某个组中的 redo 日志,要么把全部的日志都恢复掉,要么一条也不恢复。在实现上,根据多个 redo 日志的不同, 使用了特殊的 redo 日志类型作为组的结尾,来表示一组完整的 redo 日志。
Mini-Transaction
MySQL 把对底层页面中的一次原子访问的过程称之为一个Mini-Transaction,比如上边所说的修改一次 Max Row ID 的值算是一个Mini-Transaction,向某个索引对应的 B+树中插入一条记录的过程也算是一个Mini-Transaction。
一个所谓的 Mini-Transaction 可以包含一组 redo 日志,在进行崩溃恢复时这一组 redo 日志作为一个不可分割的整体。
一个事务可以包含若干条语句,每一条语句其实是由若干个 Mini-Transaction 组成,每一个 Mini-Transaction 又可以包含若干条 redo 日志,最终形成了一个树形结构。