redo log(重做日志)是 InnoDB 存储引擎独有的,它让 MySQL 拥有了崩溃恢复能力。
比如 MySQL 实例挂了或宕机了,重启时,InnoDB 存储引擎会使用 redo log 恢复数据,保证数据的持久性与完整性。
MySQL 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 Buffer Pool
中。
后续的查询都是先从 Buffer Pool
中找,没有命中再去硬盘加载,减少硬盘 IO 开销,提升性能。
更新表数据的时候,也是如此,发现 Buffer Pool
里存在要更新的数据,就直接在 Buffer Pool
里更新。
然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(redo log buffer
)里,接着刷盘到 redo log 文件里。
图片笔误提示:第 4 步 “清空 redo log buffe 刷盘到 redo 日志中”这句话中的 buffe 应该是 buffer。
理想情况,事务一提交就会进行刷盘操作,但实际上,刷盘的时机是根据策略来进行的。
小贴士:每条 redo 记录由“表空间号+数据页号+偏移量+修改数据长度+具体修改的数据”组成
一、刷盘时机
InnoDB 刷新重做日志的时机有几种情况:
InnoDB 将 redo log 刷到磁盘上有几种情况:
- 事务提交:当事务提交时,log buffer 里的 redo log 会被刷新到磁盘(可以通过
innodb_flush_log_at_trx_commit
参数控制,后文会提到)。 - log buffer 空间不足时:log buffer 中缓存的 redo log 已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。
- 事务日志缓冲区满:InnoDB 使用一个事务日志缓冲区(transaction log buffer)来暂时存储事务的重做日志条目。当缓冲区满时,会触发日志的刷新,将日志写入磁盘。
- Checkpoint(检查点):InnoDB 定期会执行检查点操作,将内存中的脏数据(已修改但尚未写入磁盘的数据)刷新到磁盘,并且会将相应的重做日志一同刷新,以确保数据的一致性。
- 后台刷新线程:InnoDB 启动了一个后台线程,负责周期性(每隔 1 秒)地将脏页(已修改但尚未写入磁盘的数据页)刷新到磁盘,并将相关的重做日志一同刷新。
- 正常关闭服务器:MySQL 关闭的时候,redo log 都会刷入到磁盘里去。
总之,InnoDB 在多种情况下会刷新重做日志,以保证数据的持久性和一致性。
我们要注意设置正确的刷盘策略innodb_flush_log_at_trx_commit
。根据 MySQL 配置的刷盘策略的不同,MySQL 宕机之后可能会存在轻微的数据丢失问题。
innodb_flush_log_at_trx_commit
的值有 3 种,也就是共有 3 种刷盘策略:
- 0:设置为 0 的时候,表示每次事务提交时不进行刷盘操作。这种方式性能最高,但是也最不安全,因为如果 MySQL 挂了或宕机了,可能会丢失最近 1 秒内的事务。
- 1:设置为 1 的时候,表示每次事务提交时都将进行刷盘操作。这种方式性能最低,但是也最安全,因为只要事务提交成功,redo log 记录就一定在磁盘里,不会有任何数据丢失。
- 2:设置为 2 的时候,表示每次事务提交时都只把 log buffer 里的 redo log 内容写入 page cache(文件系统缓存)。page cache 是专门用来缓存文件的,这里被缓存的文件就是 redo log 文件。这种方式的性能和安全性都介于前两者中间。
刷盘策略innodb_flush_log_at_trx_commit
的默认值为 1,设置为 1 的时候才不会丢失任何数据。为了保证事务的持久性,我们必须将其设置为 1。
另外,InnoDB 存储引擎有一个后台线程,每隔1
秒,就会把 redo log buffer
中的内容写到文件系统缓存(page cache
),然后调用 fsync
刷盘。
也就是说,一个没有提交事务的 redo log 记录,也可能会刷盘。
为什么呢?
因为在事务执行过程 redo log 记录是会写入redo log buffer
中,这些 redo log 记录会被后台线程刷盘。
除了后台线程每秒1
次的轮询操作,还有一种情况,当 redo log buffer
占用的空间即将达到 innodb_log_buffer_size
一半的时候,后台线程会主动刷盘。
下面是不同刷盘策略的流程图。
为0
时,如果 MySQL 挂了或宕机可能会有1
秒数据的丢失。
为1
时, 只要事务提交成功,redo log 记录就一定在硬盘里,不会有任何数据丢失。
如果事务执行期间 MySQL 挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。
为2
时, 只要事务提交成功,redo log buffer
中的内容只写入文件系统缓存(page cache
)。
如果仅仅只是 MySQL 挂了不会有任何数据丢失,但是宕机可能会有1
秒数据的丢失。
二、日志文件组
硬盘上存储的 redo log 日志文件不只一个,而是以一个日志文件组的形式出现的,每个的redo
日志文件大小都是一样的。
比如可以配置为一组4
个文件,每个文件的大小是 1GB
,整个 redo log 日志文件组可以记录4G
的内容。
它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示。
在这个日志文件组中还有两个重要的属性,分别是 write pos、checkpoint
- write pos 是当前记录的位置,一边写一边后移
- checkpoint 是当前要擦除的位置,也是往后推移
每次刷盘 redo log 记录到日志文件组中,write pos
位置就会后移更新。
每次 MySQL 加载日志文件组恢复数据时,会清空加载过的 redo log 记录,并把 checkpoint
后移更新。
write pos
和 checkpoint
之间的还空着的部分可以用来写入新的 redo log 记录。
如果 write pos
追上 checkpoint
,表示日志文件组满了,这时候不能再写入新的 redo log 记录,MySQL 得停下来,清空一些记录,把 checkpoint
推进一下。
注意从 MySQL 8.0.30 开始,日志文件组有了些许变化:
The innodb_redo_log_capacity variable supersedes the innodb_log_files_in_group and innodb_log_file_size variables, which are deprecated. When the innodb_redo_log_capacity setting is defined, the innodb_log_files_in_group and innodb_log_file_size settings are ignored; otherwise, these settings are used to compute the innodb_redo_log_capacity setting (innodb_log_files_in_group * innodb_log_file_size = innodb_redo_log_capacity). If none of those variables are set, redo log capacity is set to the innodb_redo_log_capacity default value, which is 104857600 bytes (100MB). The maximum redo log capacity is 128GB.
Redo log files reside in the #innodb_redo directory in the data directory unless a different directory was specified by the innodb_log_group_home_dir variable. If innodb_log_group_home_dir was defined, the redo log files reside in the #innodb_redo directory in that directory. There are two types of redo log files, ordinary and spare. Ordinary redo log files are those being used. Spare redo log files are those waiting to be used. InnoDB tries to maintain 32 redo log files in total, with each file equal in size to 1/32 * innodb_redo_log_capacity; however, file sizes may differ for a time after modifying the innodb_redo_log_capacity setting.
意思是在 MySQL 8.0.30 之前可以通过 innodb_log_files_in_group
和 innodb_log_file_size
配置日志文件组的文件数和文件大小,但在 MySQL 8.0.30 及之后的版本中,这两个变量已被废弃,即使被指定也是用来计算 innodb_redo_log_capacity
的值。而日志文件组的文件数则固定为 32,文件大小则为 innodb_redo_log_capacity / 32
。
关于这一点变化,我们可以验证一下。
首先创建一个配置文件,里面配置一下 innodb_log_files_in_group
和 innodb_log_file_size
的值:
[mysqld]
innodb_log_file_size = 10485760
innodb_log_files_in_group = 64
docker 启动一个 MySQL 8.0.32 的容器:
docker run -d -p 3312:3309 -e MYSQL_ROOT_PASSWORD=your-password -v /path/to/your/conf:/etc/mysql/conf.d --name
MySQL830 mysql:8.0.32
现在我们来看一下启动日志:
2023-08-03T02:05:11.720357Z 0 [Warning] [MY-013907] [InnoDB] Deprecated configuration parameters innodb_log_file_size and/or innodb_log_files_in_group have been used to compute innodb_redo_log_capacity=671088640. Please use innodb_redo_log_capacity instead.
这里也表明了 innodb_log_files_in_group
和 innodb_log_file_size
这两个变量是用来计算 innodb_redo_log_capacity
,且已经被废弃。
我们再看下日志文件组的文件数是多少:
可以看到刚好是 32 个,并且每个日志文件的大小是 671088640 / 32 = 20971520
所以在使用 MySQL 8.0.30 及之后的版本时,推荐使用 innodb_redo_log_capacity
变量配置日志文件组。
三、总结
相信大家都知道 redo log 的作用和它的刷盘时机、存储形式。
现在我们来思考一个问题:只要每次把修改后的数据页直接刷盘不就好了,还有 redo log 什么事?
它们不都是刷盘么?差别在哪里?
1 Byte = 8bit
1 KB = 1024 Byte
1 MB = 1024 KB
1 GB = 1024 MB
1 TB = 1024 GB
实际上,数据页大小是16KB
,刷盘比较耗时,可能就修改了数据页里的几 Byte
数据,有必要把完整的数据页刷盘吗?
而且数据页刷盘是随机写,因为一个数据页对应的位置可能在硬盘文件的随机位置,所以性能是很差。
如果是写 redo log,一行记录可能就占几十 Byte
,只包含表空间号、数据页号、磁盘文件偏移
量、更新值,再加上是顺序写,所以刷盘速度很快。
所以用 redo log 形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。