MySQL的日志--Redo Log【学习笔记】
MySQL的日志--Redo Log
知识来源:
- 《MySQL是怎样运行的》--- 小孩子4919
MySQL的事务四大特性之一就是持久性(Durability)。但是底层是如何实现的呢?这就需要我们的Redo Log(重做日志)闪亮登场了。它记录着事务的所有操作,等到MySQL崩溃之后,哪怕修改的脏页生前还在
Buffer Pool
中遨游,此时也可以读取Redo Log
中的信息,将这些脏页生前的样貌刻在磁盘上。有关于
Buffer Pool
的知识可以查看mysql的Buffer Pool【学习笔记】-CSDN博客。
为什么持久化需要Redo Log?
最直接的方法,可以是等事务执行完成之后,立刻进行持久化操作。那为什么还需要Redo Log?
-
直接持久化的弊端
-
如果事务执行完成之后直接持久化,那么可能会进行大量的随机IO刷盘脏页数据。这时候,性能损耗很大,用户会觉得MySQL慢慢的。
-
并且直接持久化的空窗期较大,如果在这个时候断电,那么可能会出现事务部分操作成功,部分失败的情况。
-
-
Redo Log的好处
-
Redo Log记录的是MySQL执行语句需要的必要信息,比如:插入一条记录,那么Redo Log日志就会记录字段数量,插入页面的偏移量等等信息。相较于刷脏页的16KB,这短短的几十上百字节显得微不足道。并且Redo Log是顺序IO,也就是怼着Redo Log文件追加记录。持久化的空窗期就很小,持久化的效率很高!丝毫不影响用户体验。
-
Redo Log的格式
Redo Log的通用格式
根据type的不同,对应的data内部的格式和内容也会不同。这个通用格式比作一辆车,那么type就是车的型号,型号不同,那车的样子也会不同。
-
type:Redo Log的类型,MySQL5.7.22中共有53种不同的类型。
-
space ID:表空间ID。不知道表空间的可以看InnoDB的表空间【学习笔记】-CSDN博客。
-
page number:页号。也在表空间的文章中有说明。
-
data:根据type的不同而不同。
简单的Redo Log
如果该Redo Log中记录的是修改某页中的某个字段。如:修改系统表空间页号为7的页面中Max Row ID属性。MySQL在启动时,会将这个属性读取到内存赋值给一个全局变量中,用于赋值给那些有隐藏列row_id的新插入记录,随机自增1。等到这个值为256的倍数时,就会将该值持久化。此时会先在Buffer Pool中该页对应的缓冲页中修改该值,然后再记录Redo Log。等下次系统启动时,会读取Max Row ID属性增加256后(可能重启前,该值为257,需要保证这个值大于重启前的值),再赋值给内存中的全局变量。
-
对于这种只要将修改页面中某个偏移量开始的几个字节的值持久化的情况。只需要很简单的Redo Log便可以,这种Redo Log被称为物理日志。具体类型如下:
-
MLOG_1BYTE(type字段值为1):表示页面某个偏移量处写入1个字节。
-
MLOG_2BYTE(type字段值为2):表示页面某个偏移量处写入2个字节。
-
MLOG_4BYTE(type字段值为4):表示页面某个偏移量处写入4个字节。
-
MLOG_8BYTE(type字段值为8):表示页面某个偏移量处写入8个字节。
-
MLOG_WRITE_STRING(type字段值为30):表示页面某个偏移量处写入1个字节序列。
-
复杂的Redo Log格式
对于修改一个页面的一个字段来说,简单的Redo Log已经足够了。但是如果你需要插入一条记录(如果页面剩余空间不够,可能还需要进行页分裂)。并且被修改页面中的Page Header、Page Directory中都有值需要修改。例如:Page Directoy中的槽信息,Page Header中的PAGE_N_DIR_SLOTS(槽数量)。。
这时候简单的Redo Log格式已经不够用了。需要更加抽象,上层的记录方式,比如记录插入语句的必要信息,等数据恢复的时候,执行插入语句,而不是将对应修改的字段更新。
-
这些对应的type分别是:
-
MLOG_REC_INSERT(type的值为9):表示插入一条非紧凑行格式(REDUNDANT)
-
MLOG_COMP_REC_INSERT(type的值为38):表示插入一条紧凑行格式记录(COMPACT、DYNAMIC、COMPRESSED)
-
MLOG_COMP_REC_DELETE(type的值为58):表示删除一条紧凑行格式记录(COMPACT、DYNAMIC、COMPRESSED)
-
MLOG_COMP_LIST_START_DELETE(type的值为44):表示从给定的某条记录开始删除一系列紧凑型行格式记录
-
MLOG_COMP_LIST_END_DELETE(type的值为43):与MLOG_COMP_LIST_START_DELETE相呼应,表示删除一系列紧凑型行格式记录,直到MLOG_COMP_LIST_END_DELETE记录的行格式为止
-
-
这些Redo Log的data中的内容都是逻辑语句,而不是物理层面具体的偏移量处的值。
-
这边以MLOG_COMP_REC_INSERT举例子。
-
n_uniques:表示需要n_uniques个字段才能保证记录唯一。比如:对于聚簇索引来说,就是主键列数,对于二级索引来说,就是索引列+主键列数(无论记录对应的索引列是否为NULL)。
-
field1_len~fieldn_len:若干个字段存储空间大小。无论固定长度类型(INT)还是可变长度类型(VARCHAR)。
-
offset:前一条记录在页面中的偏移量,用于恢复时更新该记录记录头中的next_record属性为新插入记录真实数据开始的偏移量。
-
end_seg_len:可以转换成该记录占用存储空间的总大小。具体转换方式不展开。
-
Mini-Transaction(MTR)
-
首先提出一个最重要的问题--有了
Transaction
为什么还需要Mini-Transaction
?-
Tansaction
包含一系列数据库操作,每个操作对应一条数据库语句。 -
而每条数据库语句底层MySQL会执行很多动作分别是一条
redo log
,比如:向满的页面插入一条记录,MySQL就会执行页分裂:开辟新页、根据主键平分原来的数据(不考虑90-10划分机制)、插入新数据、新增目录项。(当然还有更新二级索引、系统页什么的。。这边就省略了) -
这个时候如果中途崩溃了,也就是说,只记录到"新数据插入"这条
redo log
,后面的日志没了。等到时候开始恢复数据,MySQL执行这一系列记载下来的redo log
,但是目录项没有生成,那这个B+树不就出问题了? -
所以这时候需要将对底层页面的操作划分成一个一个组,每个组被称为MTR,保证原子性,一组的redo log要不全部记录,要不全部不记录。如:对聚簇索引的操作划分成一个不可分割的组,这个组内的操作需要保证原子性。
-
-
如何划分
-
如果一个语句生成多个
redo log
-
那么在这一系列
redo log
后面再加上type为MLOG_MULTI_REC_END
的redo log
(里面有且仅有type这个字段),在解析redo log
的时候,如果没有读到这个类型的redo log
,那么就会将之前解析的redo log全部丢弃,一句也不执行。
-
-
如果一个语句生成一个
redo log
-
当然按照上面的方法也可以,但是有更好的方法节省一个字节。上面提到
redo log
最多也就53中类型,那么type
的第一位就可以拿出来标记为1,代表这个需要保证原子性的操作只产生了一条单一的redo log
。(其实上面也可以采用这种方法,比如10001
,那轻易可以划分成1|0001
,但是可能考虑兼容性吧。。。)
-
-
-
Mini-Transaction在事务中的层次
-
当然不同事务是可以并发持久化的,并且持久化单元就是MTR。
-
Redo Log写入过程
存储结构---Redo Log Block
-
每个Redo Log Block占512字节。有点类似于表空间中的页,有头有尾,中间部分存放记录。一条Redo Log可以被称为Redo Log记录。
-
log block header
-
LOG_BLOCK_HDR_NO
:每个block都有编号。就像页面有页号。-
计算公式是
((lsn/512)&0x3FFFFFFF)+1
,这个lsn后面会讲到。-
此时可以看到
LOG_BLOCK_HDR_NO
最大为1G(2^30),最多存在1G个block,所以Redo Log File
最多也就512GB -
LOG_BLOCK_HDR_NO
第一位是flush bit
,该值为1,则表示本block是在将Redo Log Buffer
中的block落盘操作中,第一个被刷入的block
-
-
-
LOG_BLOCK_HDR_DATA_LEN
:表示block使用的字节数。初始值为12字节,全部使用完则为512字节,否则则为redo log大小+12字节。 -
LOG_BLOCK_FIRST_REC_GROUP
:表示该block中第一个MTR中的第一个redo log记录偏移量。 -
LOG_BLOCK_CHECKPOINT_NO
:表示checkpoint的序号。
-
-
log block trailer
-
LOG_BLOCK_CHECKSUM
:校验和。
-
-
内存中---Redo Log Buffer
类似于缓冲页的Buffer Pool,块也有缓冲,叫做Redo Log Buffer,简称log buffer。在MySQl启动的时候向操作系统申请的连续内存空间,可以通过启动选项innodb_log_buffer_size来指定大小,默认16MB。
-
向
log buffer
中写入redo log
记录是顺序写入的,并且通过全局变量buf_free
指针来标记后续redo log
记录该从哪里开始写。 -
buf_next_to_write
指针标记后续redo log
记录从哪里开始刷入磁盘。 -
redo log
写入是以MTR为单位的,并且不同事务的MTR可以交替写入。比如:-
事务T1的两个MTR分别称为mtr_t1_1和mtr_t1_2;
-
事务T2的两个MTR分别称为mtr_t2_1和mtr_t2_2;
-
磁盘中---Redo Log File
-
Redo Log Buffer
刷盘到Redo Log File
的时机-
当
Redo Log Buffer
使用容量超过50%左右,就开始将缓存落盘。 -
默认情况下,事务提交时会立即将缓存落盘。可以通过
innodb_flush_log_at_trx_commit
系统变量设置,默认值为1。-
0:事务提交后,不立即将缓存落盘。缓存落盘的任务交给了后台线程。
-
1:事务提交后,立即将缓存落盘。(其实是先写到操作系统
page cache
中,然后调用fsync()系统调用,立即将page cache
中的数据写到磁盘中) -
2:事务提交后,立即将缓存写到操作系统的
page cache
中,具体何时写到磁盘,完全根据操作系统安排。
-
-
后台线程每秒一次,将缓存落盘。
-
正常关闭服务器
-
做checkpoint
-
-
Redo Log File
默认存放在MySQL的数据目录中,可以通过innodb_log_group_home_dir
设置。默认有两个文件--ib_logfile1
和ib_logfile2
,可以通过innodb_log_files_in_group
设置数量,文件的命名采用ib_logfile[n],n=0,1,2...。每个文件大小默认48MB,可以通过innodb_log_file_size
设置。-
书写顺序是从
ib_logfile0
开始写,写满则写ib_logfile1
,不断延续。如果全部都被写满了,则从ib_logfile0
重新开始写。 -
Redo Log File的总大小是
innodb_log_files_in_group * innodb_log_file_size
-
-
Redo Log File
的格式-
由两部分组成前四个block+后续存储Redo Log的block
-
前四个block的结构
-
log file header:描述该redo log file的整体信息
-
-
checkpoint1
-
-
checkpoint2:和checkpoint1一致
-
-
-
Log Sequence Number(LSN)
-
lsn
是MySQL
的一个全局遍历,用来标注系统中写入的Redo Log
量(包括刷入磁盘的、和存在Redo Log Buffer
中的)-
初始值为8704,后续增加多少
Redo Log
量,则增加相应的值(还包括block的头尾所占的字节)-
此时
mtr_1
为200字节,mtr_2
为1000字节,那么计算当前lsn
则为8704+200+1000+12*3+4*2=9948字节
。
-
-
-
而还有一个全局变量
flushed_to_disk_lsn
,则用来标注系统刷入磁盘的Redo Log
量。这个变量的计算方式同lsn
。 -
flush
链表(存在于Buffer Pool,专门用于记录脏页的控制块)中的lsn
-
控制块中有两个变量
oldest_modification
和newest_modification
。oldest_modification
记录第一次修改该缓冲页前的MTR
所对应的lsn
(只要该脏页的控制块存在于链表中,那么这个变量就只会赋值一次,并且按照这个值从大到小排序);newest_modification
记录最近一次修改脏页后的MTR
的lsn
值。举个例子:-
原来的链表样子
-
MTR3
修改了页b和页d,修改前的lsn
为9948,修改后的lsn为10000
-
-
checkpoint
如果脏页落盘,那么记录修改对应脏页的Redo Log也就没有存在的必要了,可以被后面新的Redo Log覆盖了,checkpoint这个机制便是判断redo log file是否可以覆盖的保证。串联了flush中的lsn和Redo Log File前四个block中LOG_HEADER_START_LSN和LOG_CHECKPOINT_LSN这两个属性。请看下面的例子:
-
现在MySQL中
Buffer Pool
、Redo Log Buffer
和Redo Log File
的样子-
在执行了
mtr_1
、mtr_2
、mtr_3
后的样子,并且除了mtr_3
,其余mtr都完成了持久化
-
-
随后
mtr_1
对应的脏页a完成了持久化,那么此时mtr_1
中的Redo Log
都失去了作用。-
此时会将该脏页的
newest_modification
赋值给磁盘中对应页File Header
中FILE_PAGE_LSN
字段(后续用于快速恢复,不知道页结构的可以看mysql如何将数据组织起来的,索引(自底向上,以innodb存储引擎为例)【学习笔记】-CSDN博客)
-
-
异步线程就会执行一次
checkpoint
,来设置Redo Log File
头部的那两个属性。(脏页落盘和执行checkpoint
虽然是有明显的先后顺序,但并不是一次脏页落盘,就执行一次checkpoint
)-
内存中有一个全局变量
checkpoint_lsn
来记录当前可以覆盖的Redo Log
总量大小,初始化为8704 -
将当前MySQL中最早修改的脏页对应的的控制块中的
oldest_modification
赋值给checkpoint_lsn
,由于脏页a已经落盘,那么最早修改的脏页就成为了脏页c,为8916。 -
内存中有一个全局变量
checkpoint_no
来记录当前checkpoint
的编号。每次执行完checkpoint
后会赋值给LOG_CHECKPOINT_NO
,随后自增1。-
当
checkpoint_no
的值是偶数时,就写到checkpoint1
中;是奇数时,就写checkpoint2
中。
-
-
将
checkpoint_lsn
赋值给LOG_CHECKPOINT_LSN
,并通过checkpoint_lsn
计算出对应Redo Log File
中的offset
赋值给LOG_CHECKPOINT_OFFSET
。 -
checkpoint完成后,各个lsn的值
-
可以通过
show engine innodb status\G
查看各个lsn
-
-
崩溃恢复
-
确定起点:首先通过比较两个
checkpoint block
的LOG_CHECKPOINT_LSN
,选出较大的checkpoint block
。并且通过其LOG_CHECKPOINT_OFFSET
获得到Redo Log File
对应的偏移量,这个便是恢复的起点。(从前面可知,lsn小于LOG_CHECKPOINT_LSN
的Redo Log
所对应的脏页已经成功落盘了。至于大于等于lsn
的Redo Log
记录无法确定是否落盘,因为脏页落盘和checkpoint
是异步的,在两者间的空窗期,发生崩溃的话,就无法通过LOG_CHECKPOINT_LSN
确定哪些脏页已经落盘了) -
确定终点:从起点开始,一路向后遍历,直到遍历到
log block header
中LOG_BLOCK_HDR_DATA_LEN
不为512字节的(如果为512字节,则表示该block全部写满Redo Log记录) -
快速恢复手段:
-
使用hash结构:将需要恢复的Redo Log记录中的表空间ID和页号作为key,Redo Log记录作为value存入hash结构,对于修改同一页面的Redo Log用链表串起来,一起执行。
-
使用磁盘页中
File Header
中的FILE_PAGE_LSN
字段:前面提到这个字段记录脏页的控制块中newest_modification
的值,也就是说如果这个值大于LOG_CHECKPOINT_LSN
,那么该脏页一定是在执行最后一次checkpoint后落盘的,那么该页相关的lsn
小于FILE_PAGE_LSN
的Redo Log
就不需要被用于恢复了(也就是直接掠过这些Redo Log
记录)
-
TIPS:此时有一个巨大的问题,也就是说,如果有一个事务执行到一半,并且前半个事务的sql语句都被记录到Redo Log中并且持久化了(innodb_flush_log_at_trx_commit设置为0,并且这个事务执行时间超过1秒)。这个时候恢复不就不能保证该事务的原子性了吗。非也,这个时候Undo Log熠熠生辉。