文章

InnoDB Redo Log

redo log 格式

redo log 介绍

我们知道数据库事务有持久性的要求,也就是一个提交的事务对数据所做的修改应该是永久有效的,即使系统崩溃也能恢复过来。

而 redo log 就是用来保证这个持久性的,redo log 也叫重做日志,意思是系统崩溃重启后按照 redo log 中记录的步骤重新更新数据页就可以恢复数据。

之所以使用 redo log 来记录变更而不是直接更新数据页,是因为直接更新数据页需要按页为单位更新并且是随机 IO 操作,效率太低了,因此引入了 redo log,它具有以下优势:

  1. redo log 每次只记录变更的数据,不会影响整个页

  2. redo log 是顺序写入磁盘的(顺序 IO)

通用 redo log 格式

一个通用的 redo log 格式如下所示:

属性

说明

type

redo log 类型(有几十种类型)

space ID

表空间 id

page number

页号

data

redo log 具体数据

简单 redo log 格式

简单 redo log 格式用于记录在某个表空间的某个页的某个偏移量处将多少字节的数据修改成了什么内容。分为以下类型:

简单 redo log 类型

说明

MLOG_1BYTE

表示在某个表空间的某个页的某个偏移量处修改了1个字节的数据

MLOG_2BYTE

表示在某个表空间的某个页的某个偏移量处修改了2个字节的数据

MLOG_4BYTE

表示在某个表空间的某个页的某个偏移量处修改了4个字节的数据

MLOG_8BYTE

表示在某个表空间的某个页的某个偏移量处修改了8个字节的数据

MLOG_WRITE_STRING

表示在某个表空间的某个页的某个偏移量处修改了一串数据

MLOG_8BYTE 格式如下:

MLOG_WRITE_STRING 格式如下:

复杂 redo log 格式

有时候执行一条语句会修改非常多的页,包括系统数据页(如 MAX ROW ID)和用户数据页。

对于插入语句,不仅要记录修改了哪些数据,还要记录一些页的元数据(如 Page Header 中的 PAGE_N_HEAP)。如果仍使用简单 redo log 格式,则需要很多条 redo log 来记录,比较浪费空间,因此 MySQL 定义了一些复杂 redo log 格式,如:

复杂 redo log

说明

MLOG_REC_INSERT

表示插入一条非紧凑行格式记录的 redo log

MLOG_COMP_REC_INSERT

表示插入一条紧凑行格式记录的 redo log

MLOG_COMP_REC_DELETE

表示删除一条紧凑行格式记录的 redo log

MLOG_COMP_PAGE_CREATE

表示创建一个存储紧凑行格式记录的页的 redo log

MLOG_COMP_LIST_START_DELETE

表示从某条记录开始删除页中一系列紧凑行格式记录的 redo log

MLOG_COMP_LIST_END_DELETE

对应 MLOG_COMP_LIST_END_DELETE,表示删除记录直到当前 redo log 为止

MLOG_ZIP_PAGE_COMPRESS

表示压缩一个数据页的 redo log

我们看一下 MLOG_COMP_REC_INSERT 内部结构:

可以看到 MLOG_COMP_REC_INSERT 没有记录PAGE_N_HEAP 的值修改为了什么等信息,而只是把在本页中插入一条记录必备的要素记了下来,当数据恢复时,再通过系统内部函数更新 PAGE_N_HEAP 等页面元数据。

n_uniques: 对于聚簇索引,该值为主键的列数;对于二级索引,该值为索引列数+主键列数

offset: 插入新记录时,需要修改前一条记录的 next_record 属性

end_seg_len: 用来间接计算当前记录占用的存储空间,相比直接存储占用的空间大小更省空间

Mini-Transaction

MySQL 把对页的一次原子访问称为一个 Mini-Transaction(mtr),如修改一次 MAX ROW ID 算一个 mtr,向某个索引插入一条记录也是一个 mtr。一个 mtr 包括一组 redo log,在进行奔溃恢复时这一组 redo log 作为一个不可分割的整体。

一个事务包括若干条语句,一条语句包括若干个 mtr,一个 mtr 又包括若干条 redo log:

如何把相应的 redo log 划分到一组?

(1) 对于会生成多条 redo log 的操作

在该组的最后一条 redo log 后面加上一条类型为 MLOG_MULTI_REC_END 的 redo log:

(2) 对于只生成一条 redo log 的操作

鉴于 redo log 类型数少于 128 种,MySQL 使用该 redo log 的 type 属性(占1个字节)的第一个 bit 位来标识是否是单一日志,值为 1 表示是单一日志,即自成一组。

redo log 写入过程

redo log block

MySQL 把 redo log 存储在大小为 512 字节的 redo log block 页中:

redo log 存储在 log block body 中,我们看下 log block header 和 log block trailer 中的属性:

属性

说明

LOG_BLOCK_HDR_NO

block 的唯一标识

LOG_BLOCK_HDR_DATA_LEN

block 已经使用的字节数(初始值为 12,写满值为 512)

LOG_BLOCK_FIRST_REC_GROUP

该 block 中第一个 mtr 的第一条 redo log 偏移量

LOG_BLOCK_CHECKPOINT_NO

checkpoint 序号

LOG_BLOCK_CHECKSUM

block 的 checksum 值

LOG_BLOCK_HDR_NO = ((LSN / 512) & 0x3FFFFFFF) + 1,可知 LOG_BLOCK_HDR_NO 的最大值为 0x40000000,即所有 block 总和为 0x40000000 * 512 = 512 GB,因此 InnoDB 规定所有 redo log 文件大小总和不能超过 512 GB。

LOG_BLOCK_HDR_NO 的第一个 bit 位称为 flush bit,如果该值为 1,表示该 block 是某次将 redo log buffer 中的 block 刷新到磁盘的第一个 block。

redo log buffer

为了进一步提高性能,InnoDB 不会将 redo log 直接写入磁盘,而是先写入一个叫 redo log buffer 的连续内存空间,这片内存由多个 redo log block 组成:

通过 innodb_log_buffer_size 可以指定 redo log buffer 的大小,默认大小为 16 MB。

redo log block 写入 redo log buffer

往 redo log buffer 中写入 redo log 是顺序的,InnoDB 定义了一个 buf_free 的全局变量来指明后续的 redo log 应该从哪里继续写入:

一个 mtr 可能包含多个 redo log,这些 redo log 不是一条条插入到 redo log buffer 中,而是每次在 mtr 结束的时候,才将这一组的所有 redo log 保存到 redo log buffer 中。

有的 mtr 产生的日志量非常大,会跨多个 redo log block 来存储。

redo log 文件

redo log 刷盘时机

redo log 不能一直在 redo log buffer 中存储,在下面的这些情况下会被刷新到磁盘中:

1. redo log buffer 空间不足时:InnoDB 认为 redo log buffer 使用了一半容量时,需要把 redo log 刷新到磁盘上。

2. 事务提交时:在事务提交时,可以不把 Buffer Pool 中对应修改的页刷新到磁盘上,但是为了保证持久性,需要把对应的 redo log 刷新到磁盘。

3. 后台定时刷新线程刷新到磁盘:后台有一个线程,大约每秒会刷新一次 redo log 到磁盘。

4. 正常关闭服务器时

5. 做 checkpoint 时

如果对持久性要求不是很强的话,事务提交时也可以选择不刷新 redo log 到磁盘,可修改 innodb_flush_log_at_trx_commit 系统变量的值:

innodb_flush_log_at_trx_commit 值

说明

0

事务提交时,不会将 redo log 刷新到磁盘,不能保证持久性

1

事务提交时,将 redo log 刷新到磁盘,可以保证持久性(默认值)

2

事务提交时,将 redo log 刷新到操作系统缓冲区,服务器挂但是操作系统没挂时可以保证持久性

redo log 文件组

保存 redo log 的磁盘文件不只一个,而是有一个文件组,这些文件以 ib_logfile[N] 的形式命名。redo log 按照顺序在文件组的各个文件中循环写入,先从 ib_logfile0 开始写入,接着 ib_logfile1、ib_logfile2、...、ib_logfileN,如果 ib_logfileN 也写满了,会重新回到 ib_logfile0 开始写入。

默认情况下,redo log buffer 中的日志会刷新到 MySQL 数据目录下的 ib_logfile0 和 ib_logfile1 这两个文件中。如果想要自定义存储 redo log 的磁盘文件,可以修改以下几个启动参数:

1. innodb_log_group_home_dir:redo log 文件所在的目录

2. innodb_log_file_size:单个 redo log 文件的大小(默认为 48 MB)

3. innodb_log_files_in_group:redo log 文件的个数

redo log 文件总大小 = innodb_log_file_size * innodb_log_files_in_group

redo log 文件格式

redo log 文件组中的每个文件的格式是一致的,都是由若干个 512 字节大小的 block 组成,可以分成两个部分:

1. 前面 2048 个字节:即前面 4 个 block,用来存储一些管理信息

2. 2048 个字节之后:用来存储 redo log buffer 刷新过来的 redo log block

不同于普通的 redo log block,下面是 redo log 文件前 4 个 block 的示意图(5.7版本):

第一个 block 是 log file header,它有以下属性:

属性

说明

LOG_HEADER_FORMAT

redo log 版本,默认为 1

LOG_HEADER_PAD1

做字节填充用

LOG_HEADER_START_LSN

当前 redo log 文件开始的 LSN 值,即文件偏移量 2048 字节处对应的 LSN 值

LOG_HEADER_CREATOR

当前 redo log 文件的创建者,正常运行创建的文件改值为 MySQL 的版本号,使用 mysqlbackup 命令创建的文件改值为 ibbackup

LOG_BLOCK_CHECKSUM

该 block 的校验值

第 3 个 block 没有使用,第 2 个和第 4 个 block 都是 checkpoint,它有以下属性:

属性

说明

LOG_CHECKPOINT_NO

做 checkpoint 的编号

LOG_CHECKPOINT_LSN

做 checkpoint 结束时的 LSN 值,系统奔溃恢复从改值开始

LOG_CHECKPOINT_OFFSET

LOG_CHECKPOINT_LSN 在 redo log 文件组中的偏移量

LOG_CHECKPOINT_LOG_BUF_SIZE

服务器做 checkpoint 时对应的 redo log buffer 大小

LOG_BLOCK_CHECKSUM

该 block 的校验值

Log Sequence Number

InnoDB 为已经写入 redo log buffer 的 redo log 数据量设计了一个称之为 Log Sequence Number 的全局变量,其含义是日志序列号,简称为 LSN

初始的 LSN 值为 8704,之后每个 mtr 写入多少字节的日志(log block header 和 log block trailer 的大小会被计入), LSN 值就增长多少。

初始 LSN 值 8704 对应的 redo log 文件偏移量为 2048。

各种 LSN 值

redo log 刷盘分为 write 和 flush 两个步骤。

flush 链表中的 LSN

在 mtr 结束时,除了会把这一组 redo log 写入到 redo log buffer 中,还要把在 mtr 执行过程中可能修改过的页面加入 Buffer Pool 的 flush 链表。

flush 链表中的脏页按照修改发生的时间顺序进行排序,也就是按照 oldest_modification 代表的 LSN 值进行排序,被多次修改的页面不会重复插入到 flush 链表中,但是会更新 newest_modification 属性的值。 

在缓存页对应的控制块中记录了这两个关于页面何时修改的属性: 

  • oldest_modification:如果某个页面被加载到 Buffer Pool 后进行第一次修改,就将修改该页面的 mtr 开始时对应的 lsn 值写入该属性。

  • newest_modification:每修改一次页面,都将修改该页面的 mtr 结束时对应的 lsn 值写入该属性,表示页面最近一次修改后对应的 lsn 值。 

查看系统的 LSN 值

可以使用 SHOW ENGINE INNODB STATUS\G 查看 InnoDB 存储引擎当前的 LSN 值:

---                                                                                                                                                                                                                 
LOG                                                                                                                                                                                                                 
---                                                                                                                                                                                                                 
Log sequence number 27244465                                                                                                                                                                                        
Log flushed up to   27244465                                                                                                                                                                                        
Pages flushed up to 27244465                                                                                                                                                                                        
Last checkpoint at  27244456                                                                                                                                                                                        
0 pending log flushes, 0 pending chkp writes                                                                                                                                                                        
34 log i/o's done, 0.00 log i/o's/second  

属性

说明

Log sequence number

系统当前的 LSN 值,即当前已经写入 redo log buffer 的 redo log 数据量

Log flushed up to

表示 flushed_to_disk_lsn 值,即当前已经写入磁盘的 redo log 数据量

Pages flushed up to

表示 flush 链表中最早被修改的页对应的 oldest_modification 值

Last checkpoint at

系统当前的 checkpoint_lsn 值

奔溃恢复

如果数据库运行过程突然挂了,在重启后可以根据 redo log 将数据库页面恢复到奔溃前的状态。

确定恢复的起点

对于 checkpoint_lsn 之前的 redo log,这些 redo log 对应的脏页都已经被刷新到磁盘,因此没必要恢复它们。

对于 checkpoint_lsn 之后的 redo log,这些 redo log 对应的脏页不确定是否被刷新到磁盘,因此需要从 checkpoint_lsn 开始读取 redo log 开始恢复页面。

redo log 文件的管理信息中 checkpoint1 和 checkpoint2 都存储了 checkpoint_lsn 的信息,更大的那个值表示最近发生的那次 checkpoint,因此要比较这两个 checkpoint_no 的大小,选择更大的那个 checkpoint_no 拿到对应的 checkpoint_lsn 和 checkpoint_offset 值,这个 checkpoint_offset 就是恢复的起点。

确定恢复的终点

对于被填满的 redo log block 来说,其 LOG_BLOCK_HDR_DATA_LEN 的值为 512,因此恢复的终点就是到第一个 LOG_BLOCK_HDR_DATA_LEN 值不为 512 的 redo log block。

如何恢复

确定了奔溃恢复的起点和终点,接下来就是具体的恢复过程了。假设 redo log 文件中有下面 5 条 redo log,通常的想法是按照顺序扫描从 checkpoint_lsn 后面的 redo log,将日志中对应的页面恢复出来:

实际上,InnoDB 使用了一些方法来加速恢复的过程:

(1) 使用哈希表

根据 redo log 的 space id 和 page number 计算哈希值,将哈希值相同的 redo log 使用链表按照生产顺序连接起来,这样就可以一次性将一个页面修复好,减少了读取页面的随机 IO,如图所示:

(2) 跳过已经刷新到磁盘的页面

我们知道 checkpoint_lsn 之后的 redo log 不确定是否刷新到磁盘,这是因为做了最近一次 checkpoint 之后,后台线程又把 LRU 链表和 flush 链表中的一些脏页从 Buffer Pool 刷新到了磁盘。那么我们如何知道 checkpoint_lsn 之后的 redo log 对映的脏页是否刷新到磁盘呢?这需要用到页面中 File Header 中的 FIL_PAGE_LSN 属性,该属性记录了最后一次修改该页面时对应的 LSN 值。如果在做了某次 checkpoint 之后有脏页被刷新到磁盘,那么这些脏页对应 FIL_PAGE_LSN 值肯定大于 checkpoint_lsn 值,因此这些页面对应的 redo log 中 lsn 小于 FIL_PAGE_LSN 的都不需要执行了。

License:  CC BY 4.0