前面我们说了为了吧buffer pool的数据持久化到磁盘上,比如修改了一条数据,不可能每次吧整个页的数据都刷新过去,这样耗费性能,innoDB就是把修改的数据记录在redo日志里,redo日志格式主要是spaceId,type,page_number,offset,Data等。Offset记录上一条数据的地址,为了修改上一条记录头部新的next record,data记录的就是真实数据。Redo日志主要关注的就是一条语句修改了多少b+树,针对其中某颗树,可能增加了页,也可能更新了叶子节点或者内节点。他会记录修改日志或者删除日志,而删除日志格式又会分为开始和结束,MLOG_COMP_LIST_START_DELETE和MLOG_COMP_LIST_END_DELETE。
Mini-transaction
以组的形式写入redo日志
一个sql语句可能修改若干个页面,比如我们前面说的一条insert语句可能修改系统表空间页号为7 页面的max row id属性,还会更新b+树聚簇索引和二级索引对应的页面。由于这些都是发生在buffer pool里,这时候都需要记录在redo日志里,于是在记录的时候,innoDB又把这些分为不同的组。
修改max row_id是不可分割的,向聚簇索引对应的b+树插入一条记录产生的redo日志不可分割,向某个二级索引b+树插入一条记录产生的redo日志不可分割,还有一些其他页面生成的redo日志不可分割。。。
怎么理解不可分割呢,我们向某个索引对应的b+树插入一记录,在插入b+树之前,需要定位向哪个叶子节点代表的数据页,定位到具体的数据页后,有两种可能:
情况一:该数据页的剩余空闲空间充足,足够容纳这一条待插记录,这种就很简单,直接把记录插入这个数据页,记录一条MLOG_COMP_REC_INSERT记录到redo日志,我们吧这种情况称为乐观插入。
情况二:该数据页的剩余空间不足,那么这时候就需要进行所谓的页分裂操作,也就是新建一个叶子节点,插入一条10的记录,那么比10大的记录都会移到新建的叶子节点,吧10插到前面的叶子节点,如果内节点不够用,则同样也需要分裂,这样会记录更多的redo日志,我们称这种为悲观插入。对于这些,我们还需要修改各种段,区的统计信息,各种链表的统计信息,(比如free链表,fsp_free_page链表等等),反正有二三十条记录。
这些操作肯定必须是原子性的,比如不能在系统宕机的时候,redo日志吧聚簇索引的修改记录恢复,而二级索引的修改记录未恢复,这种必定导致数据错误,形成不完成的b+树。如何保证原子性呢,于是innoDB规定这些操作必须以【组】的形式来记录到redo日志,在系统宕机重启时候,要么全部一起恢复,要么一条都不恢复。那么怎么做到呢:
当悲观插入,需要记录多条redo日志。innoDB解决办法是,每次一组日志之后,记录一条特殊的redo日志,MLOG_MULTI_REC_END,type字段对应的十进制数字为31,该类型的结构很简单,只有一个type字段。所以当系统崩溃时候,只有解析到MLOG_MULTI_REC_END才会算是一组完整的操作,如果没有解析到MLOG_MULTI_REC_END,则之前解析的都放弃。
如果有的操作只记录一条数据,然后海特意记录一条MLOG_MULTI_REC_END,不是很浪费吗,这时候如果type字段的第一个比特位是1,代表需要该原子性操作只需要操作一条redo日志。
Redo日志写入过程
Redo log block
innoDB为了更好的进行系统恢复,他们吧通过mtr生成的redo日志都放在大小为512个字节的页中,为了和我们前面表空间的数据页做区别,所以redo日志的页我们称为block(其实他们是差不多的)。Redo log block的结构如下,
log block header:12个字节。
Log block body:496个字节。
Log block trailer:4个字节。
其中redo日志主要存储在body里,我们在看看header 和trailer
Log block header 分为四个属性:
log_block_hdr_no:4个字节,每个block都有一个大于0的唯一标号,本属性就代表唯一值。
log_block_hdr_data_len:表示已经存入redo日志已经使用的字节,从12个字节开始,因为log block body是从12个字节处开始,如果log block body全部填满了,则记录是512字节。
log_block_first_rec_group:多条redo日志会生成一个记录组redo_log_record_group,这个log_block_first_rec_group就代表mtr生成redo日志记录组的偏移量。
log_block_checkpoint_no:表示所谓checkpoint的序号,后面着重介绍。
Log block trailer分为一个属性:
Log_block_checksum:表示block的效验值,用于正确性效验。
Redo日志缓冲区
我们前面说过,为了存储数据到磁盘,会有一个数据的缓冲区buffer pool,同理,redo日志也不能直接写到磁盘,而是需要在mysql启动前,申请一个redo log buffer的连续内存空间(redo日志缓冲区),可以称为log buffer,这篇区域划分为若干个redo log buffer。
可以通过设置启动参数innoDB_log_buffer_size来制定log_buffer的大小,在mysql5.7.21这个版本,默认参数是12mb。
Redo日志写入log buffer
向log buffer 写入redo日志是顺序的,先往前面的block中写,当前面的block满了之后,就往后面空的写,所以如何定位到空的block呢,第一个问题就是block的偏移量,所以innoDB提供了一个buf_free的全局变量来记录block的偏移量,指明redo日志写入的位子。
我们前面说了一个mtr执行过程中可能产生若干条redo日志,这些日志都是不可分割的组,所以并不是每生成一条redo日志,就将其插入log buffer中,而是将mtr产生的日志先存到一个地方,当mtr结束的时候,再将产生的一组数据全部赋值到log_buffer中。
假设我们现在有两个名为T1/T2的事务,每个事务包含两个mtr,我们给mtr命名一下:
事务T1的两个mtr分别为mtr_t1_1和mtr_t1_2。
事务T2的两个mtr分别为mtr_t2_1和mtr_t2_2。
不同的事务可能并发执行,所以T1和T2的mtr可能交替执行,每当一个mtr执行完毕,伴随着该mtr生成的一组redo日志就需要复制到log buffer 中,也就是说不同事务的mtr可能是交替写入log buffer中的。