| 前言
提及 Redo Log(重做日志)与 LSN(log sequece number)时,经常被问及以下问题:
MySQL 的 InnoDB 为什么要有 Redo Log? LSN 是什么? LSN 与 Redo Log 之间有什么相互关系? Redo Log 如何轮换? ……
基于 MySQL 8.0 的源码,以及对 InnoDB 机制一些内部探讨与分享,写了几篇关于 Redo Log 的文章。本篇先讲一下 Redo Log 的日志结构。
什么是页?
讲 Redo Log 之前,先来了解一下 Jeff Dean 对计算机系统中各种存储系统访问时间的总结[1]:
Latency Comparison Numbers--------------------------L1 cache reference 0.5 nsBranch mispredict 5 nsL2 cache reference 7 ns 14x L1 cacheMutex lock/unlock 25 nsMain memory reference 100 ns 20x L2 cache, 200x L1 cacheCompress 1K bytes with Zippy 3,000 ns 3 usSend 1K bytes over 1 Gbps network 10,000 ns 10 usRead 4K randomly from SSD* 150,000 ns 150 us ~1GB/sec SSDRead 1 MB sequentially from memory 250,000 ns 250 usRound trip within same datacenter 500,000 ns 500 usRead 1 MB sequentially from SSD* 1,000,000 ns 1,000 us 1 ms ~1GB/sec SSD, 4X memoryDisk seek 10,000,000 ns 10,000 us 10 ms 20x datacenter roundtripRead 1 MB sequentially from disk 20,000,000 ns 20,000 us 20 ms 80x memory, 20X SSDSend packet CA->Netherlands->CA 150,000,000 ns 150,000 us 150 msNotes-----1 ns = 10^-9 seconds1 us = 10^-6 seconds = 1,000 ns1 ms = 10^-3 seconds = 1,000 us = 1,000,000 nsCredit------By Jeff Dean: http://research.google.com/people/jeff/Originally by Peter Norvig: http://norvig.com/21-days.html#answers
从总结内容可知:内存的访问速度至少是 SSD 的 4 倍、磁盘顺序访问的 80 倍! 磁盘、SSD 顺序读写明显要快于随机读写,而且磁盘、SSD 对频繁的小写均不友好。因此主流的数据库采用一次读写一个块,并且使用 buffer/cache 技术尽量减少读写次数。InnoDB 称这种读写块为页。
写放大怎么办?
对于一次事务来说,写一行数据,对应页中一个记录。但是要实现事务的持久化,不光是要往磁盘中写数据页,还要写 Undo 页。这就是出现了修改一行,需要持久化多个页到磁盘中,因此性能的损失会比较大,这也就是通常所说的写放大问题。因此人们提出了先写日志 WAL(write ahead log) 的方式进行优化,即将 页 中修改的操作,转换为重做日志(Redo Log)。
在事务提交时,不需要保证修改的页持久化到磁盘中,只需保证日志已经持久化存储到磁盘中即可。如果出现掉电或者故障的场景,内存的页虽然丢失,但是可以通过磁盘的页进行 Redo 重做,恢复更改的内存页。
在绝大部分情况下,Redo Log 数据比数据页和 Undo 页要小,而且按顺序写入,性能也比写放大后的好。由此可以看出,数据库使用 Redo 对数据的操作,速度上接近内存,持久性接近磁盘。
| Redo Log 的实现方式
设计思路
InnoDB 的 Redo Log 是一组文件的集合,默认是两个。每个日志文件又由一组 512 Byte 大小的日志块组成。

图 1. 日志文件结构
每个日志文件前 4 个日志块保留。其中第一个日志文件里的前 4 块保存着 Redo 日志的元数据信息。日志文件大小在初始化就已经确定,日志块逻辑上组成一个环,循环使用。
细心的读者会发现,日志文件前 4 个保留日志块,有 2 个 checkpoint 块,不免会有如下两个疑问:
假设 checkpoint1 掉电损坏,则选择 checkpoint2 块选取前一个 checkpoint LSN 做恢复。按照 InnoDB 的轮换算法,第二次写入 checkpoint 点的位置仍然是 checkpoint1,再次写入掉电仍然只会在 checkpoint1 损坏,两个 checkpoint 块方法仍然是可靠的。
Header 日志块
Header 日志块是描述日志总体信息的块,虽然只有第一个日志文件有内容,但是 InnoDB 每个日志文件都有 Header 日志块。
| 宏 | 偏移 | 长度 | 含义 |
|---|---|---|---|
| LOG_HEADER_FORMAT | 0 | 4 | 格式 |
| LOG_HEADER_PAD1 | 4 | 4 | 补齐长度,预留字段 |
| LOG_HEADER_START_LSN | 8 | 8 | 起始 LSN,最初为固定值 4*k。如果发现有删除 Redo 文件的动作,则可能是系统表空间第一个页 page LSN 计算。 |
| LOG_HEADER_CREATOR | 16 | 32 | 日志文件名称 |
| LOG_HEADER_FLAGS | 48 | 4 | 特殊用途 |
| 其他空间,通常为 0 | |||
| CHECKSUM | 508 | 4 | 日志块的 checksumchecksum 用来验证 block 的是否完整和正确。 |
checkpoint 日志块
日志文件中记录检查点信息的日志块有两个,每个 checkpoint 日志块结构如下:
| 宏 | 偏移 | 长度 | 含义 |
|---|---|---|---|
| LOG_CHECKPOINT_NO | 0 | 8 | checkpoint 序号 |
| LOG_CHECKPOINT_LSN | 8 | 16 | checkpoint LSN |
| LOG_CHECKPOINT_OFFSET | 16 | 8 | checkpoint 的文件偏移 |
| 其他空间,通常为 0 | |||
| CHECKSUM | 508 | 4 | 日志块的 checksum |
普通日志块
记录日志记录信息的日志块,头 12 个字节与最后 4 个字节记录日志的描述信息,其他空间存储日志记录。日志块结构如下:
| 偏移 | 长度 | 含义 | |
|---|---|---|---|
| LOG_BLOCK_HDR_NO | 0 | 4 | 日志块的序号最高比特位是 flushbit |
| LOG_BLOCK_HDR_DATA_LEN | 4 | 2 | 块内日志长度包含头部信息与 checksum,最高位指示是否加密 |
| LOG_BLOCK_FIRST_REC_GROUP | 6 | 2 | 第一条全新日志开始位置 |
| LOG_BLOCK_CHECKPOINT_NO | 8 | 4 | 本次 checkpoint 的序号 |
| 其他空间,用以存储日志记录 | |||
| CHECKSUM | 508 | 4 | 日志块的 checksum |
示例
一条日志记录可以跨多个日志块,一个日志块可以包含多个日志记录。

图 2. 几种日志块示例
* 图中 block tailer 表示 checksum。
示例结构说明
上图中,三个日志块的 LOG_BLOCK_HDR_DATA_LEN 值都为 512; log block1 的 LOG_BLOCK_FIRST_REC_GROUP 值为 12; log block2 无全新日志,则值为 0; log block3 值为 12+ 红色部分的长度; 日志块的块号依据 LSN 位置换算。
| Redo Log 的切换写入
假设 LSN 起点为 1,每个日志文件长度为 5,下图展示了 LSN 增长时如何切换文件。

图 3. 日志切换
很显然,LSN 1~5 在第一个文件,6~10 在第二个文件,LSN 11 在第一个文件 LSN 为 1 所在位置。Redo Log 应该写在哪个文件,是可以依据 LSN 计算出来的。
那么,Redo Log 是如何将顺序写入的结构实现为一个逻辑的环呢?
| 从 LSN 到 Offset
日志在逻辑上是一个环。checkpoint LSN 表示,LSN 之前的修改的 page 已经成功持久化到磁盘中,相关的 Redo Log 的使命已经结束。作为崩溃恢复的起点,它一定是在某个 MTR 的 END LSN 位置。因此位置可能在某日志块边缘,也可能在日志块中。

图 4. Checkpoint LSN 可能在的位置
通过前面的内容得知,checkpoint 块保存的信息有 checkpoint LSN 与 LOG_CHECKPOINT_OFFSET。checkpoint offset 是 checkpoint LSN 在日志文件组中的偏移位置。因此 LSN 与 offset 计算公式如下:
size_capacity = log.n_files * (log.file_size - LOG_FILE_HDR_SIZE);
日志的容量是文件个数乘以日志文件有效空间(文件大小减去四个 logblock)。
if (lsn >= log.current_file_lsn) {delta = lsn - log.current_file_lsn; delta = delta % size_capacity;} else {/* Special case because lsn and offset are unsigned. */ delta = log.current_file_lsn - lsn; delta = size_capacity - delta % size_capacity;}
在启动时,current_file_lsn
通常是 checkpoint LSN, current_file_real_offset
通常是 checkpoint offset。LSN 比 checkpoint LSN 大,所以delta = lsn - log.current_file_lsn
表示 LSN 与 checkpoint LSN 的距离。这个距离可能会超过 size_capacity ,因此使用了取余操作。如果 LSN 比 checkpoint LSN 小呢?这说明 LSN 在 checkpoint LSN 前面。checkpoint LSN 是起点,也是终点。checkpoint LSN + size_capacity 的位置,也是checkpoint LSN 所在的位置。所以delta = size_capacity - delta % size_capacity;
与 - delta % size_capacity
是等效的,为避免 offset 计算出现负数的情况,可做如下处理:
size_offset = log_files_size_offset(log, log.current_file_real_offset);size_offset = (size_offset + delta) % size_capacity;return (log_files_real_offset(log, size_offset));
这个log_files_size_offset
是将current_file_real_offset
转换成日志文件有效空间的偏移位置,计算公式为:
current_file_real_offset - LOG_FILE_HDR_SIZE*(1 + current_file_real_offset/log.file_size)
将 curren_file_real_offset 减掉文件头的 4 个 logblock 大小,无跨文件就减一次,跨几个文件就多减几次。再加上偏移值,转换成 file_real_offset
就得到了真实的位置。
| 总结
本文介绍了 Redo Log 与各个日志块的基本结构,并通过示例说明了 Redo Log 的两个checkpoint 作用以及 LSN 如何与日志位置对应。
Redo Log 是一个非常重要的组成部分,LSN 通常作为数据库中数据变更的逻辑时钟,与 Redo Log 密切不可分,弄清 Redo Log 的作用与机制,就能轻松理解 LSN、数据库持久化这些概念。
参考
[1]. https://d-k-ivanov.github.io/docs/CheatSheets/Latency_Numbers/
文章至此。
以下是个人微信公众号,欢迎关注:

近期热文
你可能也会对以下话题感兴趣。点击链接便可查看。




