【2021-07-16】MySQL 中 redo log 与 binlog 的区别

750 阅读10分钟

请移步至 【DobbyKim 的每日一题】 查看更多的题目~

文章内容为【极客时间】课程【MySQL 实战 45 讲】第二讲的内容整理与个人思考

redo log

redo log 的作用主要体现在两个方面:

  1. 效率
  2. 安全

不知道,大家是否还记得《孔乙己》中,酒店掌柜有一个粉板,专门用来记录客人的赊账记录。

如果有人要赊账或还账,掌柜一般有两种做法:

  • 将账本翻出来,把这次赊的账加上去或扣除掉
  • 先在粉板上记下这次的账,等到打样或空闲的时候,再把粉板上的账归入到账本中

在酒店营业高峰期,掌柜一定会选择后者的做法,因为前者的操作实在是太麻烦了。掌柜每次都要从密密麻麻的账本中找到那个人的赊账记录,然后拿出算盘计算,最后再将结果写回账本,效率低的让人难以忍受;而采用先写入粉板再归入账本的第二种方式,则可以大大提升效率。

在 MySQL 当中也有这种问题,每一次更新操作都要写进磁盘,然后磁盘也要找到对应的那条记录,再更新,那么整个过程 IO 成本,查找成本都很高。为了解决这个问题,MySQL 就采用了类似酒店掌柜粉板的思路来提升效率。

而粉板和账本配合的整个过程,其实就是 MySQL 里经常说到的 WAL 技术,WAL 的全称是 Write-Ahead Logging(写前日志),它的关键点就是先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写账本。

具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log(粉板)里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,这就像打烊以后掌柜做的事。

那么,你可能会有一个疑问,为什么 InnoDB 引擎将更新的记录写入到 redo log 会比直接写入到磁盘快?redo log 的本质是什么?

其实 redo log 的本质也是磁盘,我们所谓的先写日志,再写磁盘本质上都是落盘操作。那么为什么写入 redo log 要比直接写盘速度快?因为,写日志采用的是顺序 IO,而写磁盘则是随机 IO

随机 IO 是指,我们的数据分散地分布在磁盘的不同页的不同扇区中,如果我们要找到相应的数据就需要通过寻址操作定位到指定的页,然后再到对应的扇区中寻找我们需要的数据,因此访问速度较慢。

顺序 IO 是指读写操作的访问地址连续,定位到第一块数据后,就无需重新寻址,读/写 磁头可以以最小的移动访问下一块数据。

如果每次更新操作 MySQL 都要直接写磁盘,那么每次都需要通过磁盘随机 IO 定位到记录位置,这个过程是低效的,而写入 redo log 则是使用顺序 IO的方式,效率会很高。

如果今天赊账的不多,掌柜可以等打烊后再整理。但如果某天赊账的特别多,粉板写满了,又怎么办呢?这个时候掌柜只好放下手中的活儿,把粉板中的一部分赊账记录更新到账本中,然后把这些记录从粉板上擦掉,为记新账腾出空间。

与此类似,InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么这块“粉板”总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面图所示:

write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。

write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。

而“擦掉一些记录”指的就是将 redo log 的一部分更新记录刷到磁盘中。

redo log 除了能够提升效率之外,也起到了保证数据安全的作用。

有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的更新记录都不会丢失,还可以从日志中恢复,这种能力叫做 crash-safe。

redo log 日志刷盘

看到这一张大图,先不要慌,我会一点一点解释图中的内容。

redo log 包括两个部分:第一个是内存中的日志缓冲(redo log buffer),另一个是磁盘上的日志文件(redo log file)。我们之前提到的 redo log 指的都是磁盘上的日志文件,即:redo log file。接下来,我会告诉你,日志是怎么从内存中落盘的。

MySQL 执行一条 DML 语句,例如:update T set c = c + 1 where id = 2; 。首先执行器会先找 InnoDB 引擎取 id = 2 这一行。id 是 主键,引擎直接用树搜索找到这一行。如果 id = 2 这一行所在的数据页本来就在内存(Buffer Pool)中,就直接返回给执行器;否则需要先从磁盘读入到内存,然后再返回。

执行器拿到引擎给的行数据,将这个值加上 1,得到新的一行数据,再调用引擎接口写入这行新数据。

引擎将这行新数据更新到内存中,此时内存中的数据已经更新,但是磁盘的数据还没有同步。这里,我们将内存中已经修改,但磁盘还未同步的缓存页叫做脏页

与此同时,引擎会将这个更新操作记录到 redo log。redo log 记录的形式是物理日志,比如“在某个数据页上做了什么修改”。那么,我们是如何将内存中的这一条日志持久化到磁盘(redo log file)中的呢?

我们可以通过设置 innodb_flush_log_at_trx_commit 参数来指定 redo log 的刷盘(落盘)方式:

  1. 当参数设置为 0 时,事务提交时不立即将 redo log buffer 中的日志写到 os buffer,而是每秒刷新 一次到os buffer 并调用 fsync() 写入到 redo log file 中,当系统崩溃的时,会丢失1秒的数据。

  2. 当参数设置为 1 时,事务每次提交都会将 redo log buffer 中的日志写入到 os buffer 中并调用 fsync() 刷到 redo log file 中。这种方式可以确保每次 commit 成功,数据就都写入到 redo log file 中,即便系统崩溃也不会丢失任何数据,但是性能相对较差。

  3. 当参数设置为 2 时,每次提交都近写入到 os buffer,然后每秒调用 fsync() 将 os buffer 中的日志写入到 redo log file 中,也存在数据丢失的风险。

redo log 数据刷盘

我们已经介绍过 redo log 日志刷盘的过程。不仅仅是日志需要刷盘,我们的“脏页”也需要同步到磁盘中。这个过程叫做数据刷盘。

还是拿《孔乙己》中酒店掌柜来举例:

酒店掌柜会先在粉板上记下这次的账,等到打样或空闲的时候,再把粉板上的账归入到账本中

什么时候会触发数据刷盘呢?大体上可以分为三个场景:

  1. redo log file 写满。我们知道,如果 write pos 追上 checkpoint,表示“粉板”满了,就会触发 checkpoint。这时,MySQL 会停止所有的更新操作,将脏页刷到磁盘中,并将 checkpoint 推进。

  2. 系统内存不足,需要将脏页淘汰,此时会将脏页刷入到磁盘中。

  3. 系统空闲时,MySQL 定期将脏页刷到磁盘。

binlog

redo log 是 InnoDB 引擎特有的日志功能,而在 MySQL 的 server 层也有自己的日志:binlog(归档日志)。

binlog 日志只能用于归档,只依靠 binlog 是没有 crash-safe 能力的。所以 InnoDB 使用另外一套日志系统,也就是 redo log 来实现 crash-safe 能力。

binlog 日志格式

binlog 日志有三种格式,分别是 STATMENT,ROW 以及 MIXED

在 MySQL 5.7.7 之前,binlog 日志默认的格式是 STATEMENT , MySQL 5.7.7 之后,默认值是 ROW。日志格式可以通过 binlog-format 参数指定。

和 redo log 物理日志不同,binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 id = 2 这一行的 c 字段加 1”。

接下来,我们来看一下 binlog 日志的三种格式:

  • STATMENT

STATMENT 格式是记 SQL 语句,每一条会修改数据的 SQL 语句都会被记录到 binlog 中。它的优点是不需要记录每一行的变化,减少了 binlog 日志量,节约了 IO,从而提高了性能;缺点是在某些情况会导致主从数据不一致发生,比如执行 sysdate() 等

  • ROW

ROW 格式是记录更新前,更新后的行的内容。其优点是不会出现某些特定情况下的存储过程无法被正确复制的问题;缺点是会产生大量的日志。

  • MIXED

MIXED 格式则是基于 STATMENT 和 ROW 两种模式的混合复制。一般的复制使用 STATEMENT 格式保存 binlog,对于 STATEMENT 格式无法复制的操作则使用 ROW 来进行保存。

binlog 日志刷盘

对于 InnoDB 存储引擎而言,只有在事务提交时才会记录biglog ,此时记录还在内存中,那么 biglog是什么时候刷到磁盘中的呢?

mysql 通过 sync_binlog 参数控制 biglog 的刷盘时机,取值范围是 0-N:

  • 0:不去强制要求,由系统自行判断何时写入磁盘;

  • 1:每次 commit 的时候都要将 binlog 写入磁盘;

  • N:每 N 个事务,才会将 binlog 写入磁盘。

从上面可以看出, sync_binlog 最安全的是设置是 1 ,这也是MySQL 5.7.7之后版本的默认值。但是设置一个大一些的值可以提升数据库性能,因此实际情况下也可以将值适当调大,牺牲一定的一致性来获取更好的性能。

redo log 与 binlog 的区别

  1. redo log 是 InnoDB 引擎特有的,而 binlog 则是 MySQL server 层实现的,所有引擎都可以使用。

  2. redo log 是物理日志,它记录了数据页具体“做了什么改动”;而 binlog 是逻辑日志,记录的是原始逻辑,比如“给 id = 2 这一行的 c 字段加 1”。

  3. redo log 是循环写的,空间固定会用完;binlog 则是追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

  4. redo log 刷盘机制为事务内每执行一条就会写入一条(innodb_flush_log_at_trx_commit = 1);binlog 则是在事务提交后一次性写入到磁盘中(sync_binlog = 1)