MySQL 日志之 redo log

总结

先来看一下 MySQL 事务更新数据执行流程。

  1. 当我们想要修改DB上某一行数据的时候,InnoDB 先判断数据页是否在内存中。
    • 若为否,则从磁盘读取数据到内存中,返回数据行。
    • 若是数据页在内存中,则直接返回数据行。
  2. 执行数据更新操作,然后把数据写入内存,同时把 redo log 写入到内存。
  3. 执行 commit 操作(此 commit 是 SQL 命令操作,而不是数据的 commit 状态)。
  4. 执行 commit 命令之后,进行两段提交操作。
    1. 把内存中的 redo log 写入到磁盘中,此时 redo log 处于 prepare 状态。
    2. 把 bin log 写入到磁盘。
    3. 提交事务,把数据写入到磁盘,此时事务处于 commit 状态。
  5. 结束。

总结:当更新数据时,会写入 redo log buffer,redo log buffer 会根据一定的规则刷新到 redo log 文件中去。当 MySQL 发生故障重启时,会根据 redo log 中的 LSN 来恢复数据。

1、什么是 redo log

redo log 叫做重做日志,它是在 InnoDB 存储引擎层产生的,是保证事务持久性的重要机制。当 MySQL 服务器意外崩溃或者宕机后,保证已经提交的事务,确定持久化到磁盘中的一种措施。

redo log 是 InnoDB 所特有的一种日志,其他存储引擎没有这个日志功能。

2、为什么需要 redo log

InnoDB 是以页为单位来管理存储空间的,任何的增删改差操作最终都会操作完整的一个页。

当我们想要修改DB上某一行数据的时候,InnoDB 是把数据从磁盘读取到内存的缓冲池(buffer pool)上进行修改。这个时候数据在内存中被修改,与磁盘中相比就存在了差异,我们称这种有差异的数据为脏页。

InnoDB 对脏页的处理不是每次生成脏页就将脏页刷新回磁盘,因为此时的刷新是一个随机io,这样会产生海量的IO操作,严重影响 InnoDB 的处理性能。

但是如果不立即刷新的话,数据此时还在内存中,如果此时发生系统崩溃最终数据会丢失的,因此权衡利弊,引入了 redo log,也就是说修改完后不立即刷新,而是记录一条日志,日志内容就是记录哪个页面,多少偏移量,什么数据发生了什么变更。这样即使系统崩溃,也可以根据 redo log 进行数据恢复。

注意,redo log 是循环写入固定大小的文件。

3、以组的方式写入

在一个事务中,可能会发生多次的数据修改,对应的就是多个数据页多个偏移量位置的字段变更,也就是说会产生多条 redo log,而且因为在同一个事物中,这些 redo log 也是不可再分的,也就是说,一个组的 redo log 在持久化的时候,不能部分成功,部分失败,否则的话,就会破坏事务的原子性。

另外为了提升性能 redo log 是按照块组织在一起,然后写入到磁盘中的,类似于数据的页,而且引入了 redo log buffer,默认的大小为16MB。buffer 中分了很多的 block,每个 block 的大小为512kb,每一个事务产生的所有 redo log 称为一个 group。

4、redo log 刷盘

redo log 包括两部分内容,分别是内存中的日志缓冲(redo log buffer)和磁盘上的日志文件(redo log file)。这样的设计同样也是为了调和内存与磁盘的速度差异。

MySQL 每执行一条 DML 语句,会先把记录写入 redo log buffer(用户空间),再保存到内核空间的缓冲区 OS-buffer 中,后续某个时间点再一次性将多个操作记录写到 redo log file(刷盘)。

这种先写日志,再写磁盘的技术,就是 WAL(Write Ahead Log)

redo log 的刷盘共有以下几种情况:

  1. 开启事务,事务提交时把对应的 redo log 写入到磁盘中去,可以通过参数 innodb_flush_log_at_trx_commit 进行配置,参数值含义如下:

    • 0:称为延迟写,事务提交时不会立即将 redo log buffer 中日志写入到 OS buffer,而是每秒写入 OS buffer 并调用写入到 redo log file 中。
    • 1:称为实时写,实时刷。事务每次提交都会将 redo log buffer 中的日志写入 OS buffer 并保存到 redo log file 中。
    • 2:称为实时写,延迟刷。每次事务提交写入到 OS buffer ,然后是每秒将日志写入到 redo log file。
    • 为了保证事务的持久性,推荐使用1.
  2. 当 log buffer 中已经使用的内存超过一半时,也会触发刷盘操作。

  3. 每秒刷新一次。刷日志的频率由变量 innodb_flush_log_at_timeout 参数的值决定,默认是1秒。需要注意的是,刷日志的频率和是否执行了commit操作无关。

  4. 当有 checkpoint 时,checkpoint 在一定程度上代表了刷到磁盘时日志所处的LSN位置。

被动刷脏页的时机:

  1. redo log 写满了, 需要将 checkpoint 向前推进, 以便继续写入日志。checkpoint 向前推进时,需要将推进区间涉及的所有脏页刷新到磁盘。

  2. 内存不足,需要淘汰一些内存页(最久未使用的)给别的数据页使用。

    • 此时如果是干净页, 则直接拿来复用。
    • 如果是脏页,则需要先刷新到磁盘(直接写入磁盘, 不用管 redo log, 后续 redo log 刷脏页时会判断对应数据页是否已刷新到磁盘),使之成为干净页再拿来使用。
  3. 数据库系统空闲时,当然平时忙的时候也会尽量刷脏页。

  4. 数据库正常关闭,此时需要将所有脏页刷新到磁盘。

5、MySQL 的两阶段提交

MySQL 将 redo log 的写入拆成了两个步骤:prepare 和 commit,中间再穿插写入 binlog,这就是两阶段提交。

两阶段提交就是让 redo log 和 binlog 这两个状态保持逻辑上的一致。redo log 用于恢复主机故障时的未更新的物理数据,binlog 用于备份操作。两者本身就是两个独立的个体,要想保持一致,就必须使用分布式事务的解决方案来处理。

为什么需要两阶段提交

如果不用两阶段提交的话,可能会出现这样情况:

  • 先写 redo log,crash 后 bin log 备份恢复时少了一次更新,与当前数据不一致。

  • 先写 binlog,crash 后,由于 redo log 没写入,事务无效,所以后续 binlog 备份恢复时,数据不一致。

两阶段提交就是为了保证 redo log 和 binlog 数据的安全一致性。只有在这两个日志文件逻辑上高度一致了。你才能放心的用 redo log 帮你将数据库中的状态恢复成 crash 之前的状态,使用 binlog 实现数据备份、恢复、以及主从复制。

binlog 默认都是不开启的!也就是说,如果你根本不需要 binlog 带给你的特性(比如数据备份恢复、搭建 MySQL 主从集群),那你根本就用不着让 MySQL 写 binlog,也用不着什么两阶段提交,只用一个 redo log 就够了。无论你的数据库如何 crash,redo log 中记录的内容总能让你MySQL内存中的数据恢复成 crash 之前的状态。

6、如何开启 redo log

innodb_support_xa=true:支持 xa 两段式事务提交,默认开启。

7、redo log 日志格式

redo log buffer (内存中)是由首尾相连的四个文件组成的,它们分别是:ib_logfile_1、ib_logfile_2、ib_logfile_3、ib_logfile_4

  • write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。
  • checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
  • write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以用来记录新的操作。
  • 如果 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。
  • 有了 redo log,当数据库发生宕机重启后,可通过 redo log 将未落盘的数据(check point之后的数据)恢复,保证已经提交的事务记录不会丢失,这种能力称为crash-safe

8、当数据库 crash 后,如何恢复未刷盘的数据到内存中

根据 redo log 和 binlog 的两阶段提交,未持久化的数据分为几种情况:

  • change buffer 写入,redo log fsync 到磁盘但未 commit,binlog 未 fsync 到磁盘,这部分数据丢失。
  • change buffer 写入,redo log fsync 到磁盘但未 commit,binlog 已 fsync 到磁盘,先从 binlog 恢复 redo log,再从 redo log 恢复 change buffer。
  • change buffer 写入,redo log 和 binlog 都已经 fsync 到磁盘,直接从 redo log 里恢复。

9、如何判断 binlog 和 redo log 是否达成了一致

当 MySQL 写完 redo log 并将它标记为 PREPARE 状态时,会在 redo log 中记录一个XID,它全局唯一的标识着这个事务。

而当你设置 sync_binlog=1 时,做完了上面第一阶段写 redo log 后,MySQL 就会更新 binlog 并且会将其刷新到磁盘中。binlog 结束的位置上也有一个XID。只要这个 XID 和 redo log 中记录的XID是一致的,MySQL 就会认为 binlog 和 redo log 逻辑上一致。就上面的场景来说就会 commit,而如果仅仅是 redo log 中记录了XID,binlog 中没有,MySQL 就会 RollBack。

对于处于 PREPARE 状态的事务,存储引擎既可以提交,也可以回滚,这取决于目前该事务对应的 binlog 是否已经写入硬盘。这时就会读取最后一个 binlog 日志文件,从日志文件中找一下有没有该 PREPARE 事务对应的 XID 记录,如果有的话,就将该事务提交,否则就回滚。

10、LSN(Log Sequence Number)

LSN 实际上就是 InnoDB 使用的一个版本标记的计数,它是一个单调递增的值。数据页和 redo log 都有各自的 LSN。我们可以根据数据页中的 LSN 值和 redo log 中 LSN 的值判断需要恢复的 redo log 的位置和大小。

11、checkpoint

如果重做日志可以无限增大,同时缓冲池也足够大,能够缓存数据库中的所有数据,那么我们就不需要再把缓冲池中页中的新版本刷新回磁盘。因为发生宕机时,我们可以通过重做日志来恢复,但一般这种情况是不满足的。

checkpoint 技术的目的就是解决以下问题:

  • 数据库发生宕机时,缩短数据库恢复时间。
  • 缓冲池不够用时,将脏页刷新回磁盘。
  • 重做日志不可用时,将脏页刷新回磁盘。

宕机恢复

数据库宕机后重启,InnoDB 会首先去查看数据页中的 LSN 的数值。这个值代表数据页被刷新回磁盘的 LSN 的大小。然后再去查看 redo log 的 LSN 的大小。

如果数据页中的 LSN 值大说明数据页领先于 redo log 刷新回磁盘,不需要进行恢复。反之需要从 redo log 中恢复数据。

当缓冲池不够用时

根据LRU算法会溢出最近最少使用的页,若此页为脏页,那么需要强制执行 Checkpoint,将脏页刷新回磁盘。

rodo log 出现不可用

当前事务数据库系统对 rodo log 的设计是循环使用的,不是让其无限增大的。

rodo log 被重用的部分,是指这部分的 rodo log 已经不再需要,即发生宕机时,不再用来恢复数据,可以被覆盖重用。

若此时 rodo log 还需要使用,那么必须强制产生 checkpoint,将缓冲池中的页至少刷新到当前 rodo log 的位置。

Reference


MySQL 日志之 redo log
https://flepeng.github.io/041-MySQL-41-底层原理-MySQL-日志之-redo-log/
作者
Lepeng
发布于
2020年8月8日
许可协议