19.1 事先说明
不必理会
19.2 redo log 是个啥
我们想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来,只需要把修改了哪些东西记录一下就好。这样也就满足了持久性的要求,记录的内容也被称为重做日志(redo log)。
19.3 redo log 格式
名称 | 含义 |
---|---|
type | 该条 redo log 的类型 |
space ID | 表空间 ID |
page number | 页号 |
data | 该条 redo 日志的具体内容 |
19.3.1 简单的 redo log 类型
redo log 中只需要记录一下在某个页面的某个偏移量处修改了几个字节的值,具体被修改的内容是什么就好了。这种极其简单的 redo log 被称为物理日志,并且根据在页面中写入数据的多少划分了几种不同的日志类型:
日志类型 | 含义 |
---|---|
MLOG_1BYTE(type=1) | 表示在页面的某个偏移量处写入 1 个字节的 redo log |
MLOG_2BYTE(type=2) | 表示在页面的某个偏移量处写入 2 个字节的 redo log |
MLOG_4BYTE(type=4) | 表示在页面的某个偏移量处写入 4 个字节的 redo log |
MLOG_8BYTE(type=8) | 表示在页面的某个偏移量处写入 8 个字节的 redo log |
MLOG_WRITE_STRING(type=30) | 表示在页面的某个偏移量处写入一串数据 |
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
19.3.2 复杂一些的 redo log 类型
把一条记录插入到一个页面时需要更改的地方非常多,简单日志类型无法满足。
日志类型 | 含义 |
---|---|
MLOG_REC_INSERT(type=9) | 表示插入一条使用非紧凑行格式的记录 |
MLOG_COMP_REC_INSERT(type=38) | 表示插入一条使用紧凑行格式的记录 |
MLOG_COMP_PAGE_CREATE(type=58) | 表示创建一个存储紧凑行格式的记录的页面 |
MLOG_COMP_REC_DELETE(type=42) | 表示删除一条使用紧凑行格式的记录 |
MLOG_COMP_LIST_START_DELETE(type=44) | 表示某条给定记录开始删除页面中的一系列使用紧凑行格式的记录 |
MLOG_COMP_LIST_END_DELETE(type=43) | 表示删除页面中的一系列使用紧凑行格式的记录直接某条给定记录结束 |
MLOG_ZIP_PAGE_COMPRESS(type=51) | 表示压缩一个数据页的 redo log |
PS:紧凑行格式 = COMPACT 行格式
- n_uniques:需要几个字段才能确保记录的唯一性。对于聚簇索引来说,n_uniques 就是主键的列数,对于其他二级索引,该值为索引列数 + 主键列数,对于唯一二级索引,因为索引值可能为 NULL,所以仍然是索引列数 + 主键列数。
- field1_len~fieldn_len:表示该记录各字段占用存储空间的大小。
- offset:表示前一条记录在页面中的地址。因为插入时要修改前一条记录的 next_record 属性。
- end_seg_len:可以间接计算出一条记录占用存储空间的总大小,包括额外信息和真实信息。
- mismatch_index:可以忽略
这种类型的日志并没有记录实际的值的变更,而是把在本页面中插入一条记录所有必备的要素记录下来,等到系统崩溃重启时,服务器会调用相关函数,将该日志作为参数,将系统恢复到崩溃之前的样子。
19.3.3 redo log 格式小结
redo log 会把事务在执行过程中对数据库所做的所有修改都记录下来,在之后系统崩溃重启后可以把事务所做的任何修改都恢复出来。
19.4 Mini-Transaction
19.4.1 以组的形式写入 redo log
-
某些操作,如向某个索引对应的 B+ 树中插入一条记录的过程必须是原子的,不能说插了一半就停止了。
-
使用
MLOG_MULTI_REC_END
类型日志来作为隔断,把多条 redo log 分割成组。针对组内的 redo log,要么全部恢复,要么一条也不恢复。
- 有的需要保证原子性的操作只生成一条 redo log,为它单独加一条
MLOG_MULTI_REC_END
的 redo 日志就不划算了。使用 type 字段的第 1 个比特位是 1 与否,来表示是否一条单一的日志
19.4.2 Mini-Transaction 的概念
对底层页面的中一次原子访问称为一个 Mini-Transaction,简称 mtr。
一个事务可以包含若干条语句,每一条语句其实是由若干个 mtr 组成,每一个 mtr 又可以包含若干条 redo log。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
19.5 redo log 的写入过程
19.5.1 redo log block
通过 mtr 生成的 redo log 都被存放在大小为 512 字节的页中,称为block。
名称 | 大小 | 含义 |
---|---|---|
LOG_BLOCK_HDR_NO | 4字节 | 表示 block 都有的大于0的唯一标号 |
LOG_BLOCK_HDR_DATA_LEN | 2字节 | 表示 block 中已经使用了多少字节,初始值12(header),最大512 |
LOG_BLOCK_FIRST_REC_GROUP | 2字节 | 表示 block 里第一个 mtr 生成的第一条 redo log 的偏移量 |
LOG_BLOCK_CHECKPOINT_NO | 4字节 | 表示 checkpoint 的序号 |
LOG_BLOCK_CHECKSUM | 4字节 | 表示 block 的校验值,用于正确性校验 |
19.5.2 redo log 缓冲区
写入 redo log 时不直接写到磁盘上,而是在服务器启动时就向操作系统申请了一大片称为 redo log buffer 的连续内存空间,简称 log buffer。这块内存空间被划分为若干个连续的 redo log block。
19.5.3 redo log写入log buffer
向 log buffer 中写入 redo log 的过程是顺序的,先写前面的 block,再写后面的block。
使用全局变量buf_free
来标记 redo log 应该写入到 log buffer 的哪个位置。
一个 mtr 产生的 redo log 是一个不分割的组,所以会先暂存到一个地方,当 mtr 结束时,将组中的 redo log 全部复制到 log buffer 中。
19.6 redo日志文件
19.6.1 redo日志刷盘时机
-
log buffer 空间不足时
当 log buffer 装满一半左右,就需要把这些日志刷新到磁盘上。通过
innodb_log_buffer_size
指定 log buffer 的大小。 -
事务提交时
事务提交时可以不把修改过的 Buffer Pool 页面刷新到磁盘,但是为了保证持久性,必须把修改这些页面的 redo 日志刷新到磁盘。
-
后台线程刷
后台有一个每秒刷新一次 log buffer 中的 redo 日志的线程。
-
正常关闭服务器时
-
做 checkpoint 时
-
其他情况……
19.6.2 redo log 文件组
show VARIABLES like 'datadir';
log buffer 中的日志默认刷新到其中的 ib_logfile0 和 ib_logfile1,可以通过参数修改。
两个文件交替写,一个写满了写另一个。
19.6.3 redo log文件格式
将 log buffer 中的 redo 日志刷新到磁盘的本质就是把 block 的镜像写入日志文件中,所以 redo 日志文件也是由若干个 block 组成。
redo 日志文件组中的每个文件大小都一样,格式也一样,由两部分组成:
- 前 2048 字节,也就是前4个 block 用来存储管理信息
- 之前全部用来存储 log buffer 中的 block 镜像
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
log file header:描述该 redo 日志文件的一些整体属性,它的结构如下:
属性名 | 长度(单位:字节) | 描述 |
---|---|---|
LOG_HEADER_FORMAT | 4 | redo 日志的版本,在 MySQL5.7.21 中该值永远为1 |
LOG_HEADER_PAD1 | 4 | 做字节填充用的,没什么实际意义,忽略~ |
LOG_HEADER_START_LSN | 8 | 标记本 redo 日志文件开始的LSN值,也就是文件偏移量为2048字节初对应的 LSN 值(关于什么是LSN我们稍后再看哈,看不懂的先忽略)。 |
LOG_HEADER_CREATOR | 32 | 一个字符串,标记本 redo 日志文件的创建者是谁。正常运行时该值为 MySQL 的版本号,比如: “MySQL 5.7.21” ,使用mysqlbackup 命令创建的 redo 日志文件的该值为 “ibbackup” 和创建时间。 |
LOG_BLOCK_CHECKSUM | 4 | 本block的校验值,所有block都有,我们不关心 |
checkpoint1:记录关于 checkpoint 的一些属性,结构如下:
属性名 | 长度(单位:字节) | 描述 |
---|---|---|
LOG_CHECKPOINT_NO | 8 | 服务器做 checkpoint 的编号,每做一次 checkpoint ,该值就加1。 |
LOG_CHECKPOINT_LSN | 8 | 服务器做 checkpoint 结束时对应的 LSN 值,系统奔溃恢复时将从该值开始。 |
LOG_CHECKPOINT_OFFSET | 8 | 上个属性中的 LSN 值在 redo 日志文件组中的偏移量 |
LOG_BLOCK_CHECKSUM | 4 | 本block的校验值,所有block都有,我们不关心 |
第三个 block 未使用,忽略~
checkpoint2:同checkpoint1
19.7 Log Sequeue Number
Log Sequence Number,日志序列号,用来记录已经写入的 redo 日志的全局变量,简称 lsn,初始值为 8704。
每组由 mtr 生成的 redo 日志都有一个唯一的 LSN 与其对应,LSN 越小,说明 redo 日志产生的越早。
19.7.1 flushed_to_disk_lsn
redo 日志是首先写到 log buffer 中,之后才会被刷新到磁盘上的 redo 日志文件。使用 buf_next_to_write 的全局变量,标记当前 log buffer 中已经有哪些日志被刷新到磁盘中了。
lsn 表示当前系统中写入的 redo 日志量,这包括了写到 log buffer 而没有刷新到磁盘的日志。使用 flushed_to_disk_lsn 这个全局变量来表示刷新到磁盘中的 redo 日志量。
系统第一次启动时,flushed_to_disk_lsn 和 lsn 都是初始值 8704。随着系统运行,redo 日志不断写入 log buffer,但并不会立即刷新到磁盘,lsh 和 flushed_to_disk_lsn 的值拉开了差距。
同样的,如果 lsn 和 flushed_to_disk_lsn 又变成相同的了,说明 log buffer 中的所有 redo 日志都已经被刷新到磁盘中了。
19.7.2 lsn 值和 redo 日志文件偏移量的对应关系
19.7.3 flush 链表中的 LSN
在 mtr 结束时,会把一组 redo 日志写到 log buffer。并且,把在 mtr 执行过程中可能修改过的页面加到 Buffer Pool 的 flush 链表。
当第一次修改某个缓存在 Buffer Pool 中的页面时,就会把这个页面对应的控制块插入到 flush 链表的头部,之后如果再次修改这个页面,就不再插入了。也就是说 flush 链表中的脏页是按照页面首次修改时间由大到小进行排序的。
19.8 checkpoint
redo 日志文件组的容量是有限的,所以需要循环使用。
但是这会造成最后写的 redo 日志与最开始写的 redo 日志追尾。
但是 redo 日志只是为了系统崩溃后恢复脏页用的,如果脏页已经刷新到了磁盘,redo 日志也就没有存在的必要的。
所以判断某些 redo 日志占用的磁盘空间是否可以覆盖的依据就是它对应的脏页是否已经刷新到磁盘了。
使用一个全局变量 checkpoint_lsn 来代表当前系统中可以被覆盖的 redo 日志总量是多少。比如当页 a 被刷新到磁盘,mtr_1 生成的 redo 日志就可以被覆盖了,就增加 checkpoint_lsn 的值,这个过程称为做一次 checkpoint。
checkponit 的步骤如下:
- 计算当前系统中可以被覆盖的 redo 日志对应的 lsn 最大值
- 将 checkpoint_lsn 和对应的 redo 日志文件组偏移量以及此次 checkpoint 的编号写到日志文件中的管理信息。
19.9 用户线程批量从 flush 链表中刷出脏页
如果当前系统修改页面的操作十分频繁,导致大量写日志操作,系统 lsn 值增长过快,可能就需要用户线程同步地从 flush 链表中把那些最早修改的脏页刷新到磁盘了。
19.10 查看系统中的各种 LSN 值
SHOW ENGINE INNODB STATUS;
19.11 innodb_flush_log_at_trx_commit 的用法
3个可选值:
- 0:表示事务提交时不立即向磁盘中同步 redo 日志,而是交给后台线程处理。速度快但会丢数据。
- 1:表示事务提交时立即向磁盘中同步 redo 日志。可以保证持久性,默认值。
- 2:表示事务提交时需要将 redo 日志写到操作系统的缓冲区中,但不保证能写到磁盘,这样如果数据库挂了但操作系统没挂,还是可以保证持久性的。
19.12 崩溃恢复
19.12.1 确定恢复的起点
checkpoint_lsn
19.12.2 确定恢复的终点
第一个没有被填满(512)的 block。
19.12.3 怎么恢复
按照 redo 日志的顺序依次扫描,再按照日志中记载的内容将对应的页面恢复出来。使用以下方法加快这一过程。
-
使用哈希表
根据 redo 日志的 space ID 和 page number 属性计算出散列值,把 space ID 和 page number 相同的 redo 日志放到哈希表的同一个槽里,哈希冲突时,用链表把他们连接起来。之后遍历哈希表进行恢复。由于一个槽里的 redo 日志是同一个页面的,避免了很多读取页面的随机 I/O。
-
跳过已经刷新到磁盘的页面
页面的 File Header 里的 FIL_PAGE_LSN 属性记录了最近一次修改页面时对应的 lsn 值。如果在做了某次 checkpoint 之后有脏页被刷新到磁盘中,那么该页面对应的 FIL_PAGE_LSN 值肯定大小 checkpoint_lsn 的值,这样的就不需要刷新到页面了。
19.13 遗漏的问题:LOG_BLOCK_HDR_NO是如何计算的
LOG_BLOCK_HDR_NO = ((lsn / 512) & 0x3FFFFFFFUL) + 1
小于 1GB
19.14 总结
- redo 日志记录了事务执行过程中都修改了哪些内容
- 事务提交只将执行过程中产生的 redo 日志刷新到磁盘,而不是将所有修改过的页面都刷新到磁盘
- 一个 mtr 可以包含一组 redo 日志,它们是一个整体不可分割
- redo 日志存放在大小为512字节的 block 中
- redo 日志缓冲区是一片连续的内存空间,由若干个 block 组成
- redo 日志文件组由若干个日志文件组成,每个日志文件大小一样,格式一样,循环使用
- lsh 指已经写入的 redo 日志量;flushed_to_disk_lsn 指刷新到磁盘中的 redo 日志量
- 执行一次 checkpoint 的意思就是增加 checkpoint_lsn 的值
- 恢复过程的起点是 checkponit_lsn,终点是第1个没写满的 block