MySQL 基础技术(三)—— MySQL 如何保证数据不丢失?

avatar
工程师 @字节跳动

之前,有一年多的工作客户端领域的工作经验。
后来,也在字节做了一年多的后端业务。
现在希望做一些 MySQL 总结,丰富一下自己在后端领域的积累。
目录如下:
MySQL 基础技术(一) —— MySQL 是如何查询的?
MySQL 基础技术(二) —— MySQL 是如何更新的?
MySQL 基础技术(三)—— MySQL 如何保证数据不丢失?
MySQL 基础技术(四)—— MySQL 如何保证高可用?


一、引子

上篇:《MySQL 是如何更新的?》中,我们介绍了MySQL的两个重要的日志:binlog 和 redo log。

其中,binlog 对应 MySQL 在 Server 层的逻辑日志。
redo log 对应 MySQL 在 InndoDB 引擎下的 Engine 层物理日志。
为了保证数据的一致性, MySQL 用一个事务将两个日志的写逻辑的一致性。(即“两阶段提交”)

两阶段提交

MySQL 的两阶段提交 + WAL技术(Write-Ahead Logging,先写日志再写盘),这两个结合在一起保证了数据不会丢失。

即:只要 redo log 和 binlog 持久化到磁盘,即使 MySQL 异常,重启后数据依然可以恢复。
那么,redo log 和 binlog 的写入流程具体是怎样的?具体是怎么保证的?

本篇,让我们一起详细了解下 redo log 和 binlog 的写入流程。


二、binlog 写入流程

首先,我们先了解下MySQL Server 层日志 —— binlog 的写入具体流程。

其实,binlog 的写入流程比较简单:

  1. 先把 binlog 日志写到 binlog cache。
  2. 事务提交时,将 binlog cache 写入 binlog 文件。

系统给每个线程的 binlog cache 分配了一片内存,大小为 binlog_cache_size。 如果超过这个大小,就会把cache暂存到磁盘的临时文件(tmp files)中。

这个很好理解,binlog cache 由内存和临时文件组件。
因为内存是有限的,在事务完成前不能直接写入最终日志文件,因此只能先临时写到临时文件(tmp files)中。

binlog写入流程

我们可以看到,每个线程都有自己的 binlog cache。但是,都共用同一份binlog文件。

write:

把日志写入文件系统内核的 page cache,page cache是 OS 对磁盘IO的缓存。

适合小文件传输。因为大文件传输 page cache 命中率低,这时不仅没有起到缓存作用,反而增加了一次数据从磁盘 buffer 到 内核 page cache 的开销。

fsync:

将数据持久化到磁盘。

fsync 才占磁盘的 IOPS(Input/Output Operations Per Second)。

MySQL 有一个参数:sync_binlog ,用来控制 write 和 fsync 的时机。 可以根据业务场景的需要,来具体调整。

sync_binlog含义
等于 0每次事务提交只write,不fsync。(不推荐)
等于 1每次事务提交不仅write,都会执行 fsync。(这个配置是最安全的,不会丢binlog日志)
等于 N每次提交事务都write,累积N个事务后,一起fsync。(性能好,但是异常重启会丢N个事务的binlog日志)

三、redo log 写入流程

了解了 binlog 的写入流程,下面我们来看看 redo log 的写入流程。

  1. 首先,写入 redo log buffer 。
  2. 其次,写入(write)文件系统的 page cache。
  3. 最后,持久化(fsync)到磁盘 disk

是不是感觉和 binlog 有点类似?

但实际上他们之间还是有很大差异的,下面我们了解下他们之间的差异。

InnoDB 提供了 innodb_flush_log_at_trx_commit 参数来控制 redo log 写入流程。

innodb_flush_log_at_trx_commit含义
= 0每次事务提交时,redo log 只会留在 redo log buffer。(风险大,等待每秒 write + fsync 到 disk)
= 1每次事务提交时,都将所有 redo log fsync到磁盘。(最安全)
= 2每次事务提交时,都将 redo log write 到 page cache。

每秒刷盘机制

InnoDB 有一个后台线程,每隔1秒,就会把 redo log buffer中的日志,调用 write 写到 page cache,然后 fsync 持久化到磁盘。

需要注意的是,事务执行中的 redo log 也是存在于 redo log buffer 的,也会被一起持久化到磁盘。(也就是说,一个还没有提交事务的 redo log,也可能已经被持久化到磁盘了)

强制刷盘

其实,不光每秒刷盘会提前持久化 redo log 到磁盘。

  • 当 redo log buffer 到达 innodb_log_buffer_size (缓冲池大小,默认是8MB)一半的时候,会主动触发 write 到文件系统的 page cache。
  • 并行事务提交,顺带将其他事务的 redo log buffer 持久化到磁盘。
    举例: 事务A 执行到一半,写入了部分 redo log 到 buffer 中。 事务B 完成,进行提交。 如果innodb_flush_log_at_trx_commit设置为1,代表每次提交都会全部fsync到磁盘。这时候,事务A的 redo log 也有部分已经持久化了。

这时候,有同学可能会问:
两阶段提交,不是最后一步才会 fsync 到磁盘上么?为什么提前持久化了呢?会有啥影响吗?

首先,prepare 阶段提前fsync到磁盘并没有问题。
因为 binlog 还没有 ready。还没到最后的 commit,并没有实际执行。
其次,磁盘IOPS是有瓶颈的。MySQL这样设计可以降低磁盘IO,提高性能。

四、双“1”配置,最安全

只有在 sync_binlog 和 innodb_flush_log_at_trx_commit 都等于1的情况下,才能保证数据不丢失。 即

  • 写 redo log 时,每次事务提交时,都将所有redo log fsync到磁盘
  • 写 binlog 时,每次事务提交时,binlog 都会执行 fsync到磁盘。

虽然,双1配置可以保证数据安全性。但是往往越安全,性能就会不如意。
在某些性能要求比较高的场景下,往往会故意打破双1配置。
(虽然在 MySQL异常时,会丢一些数据。但在大量数据的基数下,风险依然可控。)

问:为什么双“1”配置就一定是最安全的?能证明么?
答:别着急,等看完“五、真正的两阶段提交”,咱就明白了。

五、真正的两阶段提交

两阶段提交

之前,给大家介绍两阶段提交时,我画了这张图,比较好理解。 但其实由于MySQL redo log 组提交优化,真正的两阶段提交如下图:

真正的两阶段提交

MySQL这样设计的好处是,可以组提交,交叉fsync。 一组 fsync收集的write越多, 对应的磁盘的IO越少,提高MySQL性能。

因此,WAL的优势在于:

  1. redo log & binlog 都是按顺序写入磁盘的,比随机写磁盘速度快。
  2. 组提交机制,合并fsync。大幅度降低磁盘的IOPS消耗,提高IO性能。

证明数据安全

上图分为五个阶段,这样,我们在双“1”配置下,利用反证法来验证数据安全性。

  1. 如果 MySQL 挂在了 1,2,3 阶段。

这时候,不论redo log 还是 binlog 都还没有 fsync 到磁盘。
因此,掉电导致内存丢失,实际也没写入磁盘,数据一致性。

  1. 如果 MySQL 挂在了第 4 阶段 fsync binlog。 即 redo log fsync 成功, binlog fsync 失败。
    MySQL重启后,发现有 redo log 的磁盘数据,没有binlog磁盘数据。
    发现是redo log处于prepare阶段,回滚(删除磁盘里的redo log)。

  2. 如果 MySQL 挂在了第 5 阶段 commit。 即 redo log 和 binlog 都 fsync 成功,但 commit 失败。
    MySQL重启后,发现有完整的binlog和redo log,继续 commit。
    数据成功写入MySQL。

至此,我们理解了 MySQL 是如何保证数据不丢失的。
下篇,我们会介绍,MySQL是如何保证高可用的。