MySQL是如何保证不丢失数据的呢?

共 5909字,需浏览 12分钟

 ·

2021-09-25 23:01

微信公众号:欢少的成长之路

介绍

大家好,我是Leo,从事Java后端开发。之前的文章大概介绍了WAL机制,如果不太清楚的小伙伴下面第一部分我们可以再回顾一下。今天这里主要介绍一下WAL的安全性这一块。

写作思路

根据读者与朋友的反馈,所以从这篇文章开始我会加一个写作的思路。可以先让读者了解到学完这一篇下来之后能收获到哪些知识,以防看了半个小时最后啥也没学到,这样的确挺气人的。

步入正题

binlog 写入机制

binlog写入日志这个是比较简单的。提到binlog,必然提到binlog cache。那么binlog cache是什么?

我们有必要了解一些前提知识,再学习binlog。会的人可以跳过这段,照顾一下不懂的朋友。

binlog cache是一个二进制日志文件的缓冲区,他是由一个参数 binlog_cache_size 控制大小的缓冲区。

一个事务在执行是时候是不允许被拆开的,因此无论事务多大,都是要一次性保存执行的。那么这个就涉及到了binlog cache 的保存问题。如果所占的内存大小超过了这个binlog_cache_size 参数的设定。就会采用暂存到磁盘。事务在提交的时候,会先把binlog cache里的数据写入到binlog中,并清空binlog cache数据。

如下图,我们可以从图中了解一下。

每个binlog_cache是由单独的一个线程享有的。也就是说多个线程带着多个binlog_cache去进入write操作的时候是写入到一个binlog 文件的。效率是非常快的,因为并没有涉及到磁盘IO的开销。

当进行到了fsync的时候,才是将数据持久化到磁盘操作。这个时候才会占用磁盘IO,也就是我们常说的IOPS。

我们我们可以深入讨论一个问题,什么时候进行write,什么时候需要fsync操作呢?

下面我们介绍一下write与fsync的时机。

int sync_binlog=0;
if(sync_binlog==0){
每次提交事务都只 write,不 fsync
}
if(sync_binlog==1){
每次提交事务都会执行 fsync
}
if(sync_binlog>1){
每次提交事务都 write,但累积 N 个事务后才 fsync。
}

因此,在出现 IO 瓶颈的场景里,将 sync_binlog 设置成一个比较大的值,可以提升性能。在实际的业务场景中,考虑到丢失日志量的可控性,一般不建议将这个参数设成 0,比较常见的是将其设置为 100~1000 中的某个数值。

但是,将 sync_binlog 设置为 N,对应的风险是:如果主机发生异常重启,会丢失最近 N 个事务的 binlog 日志。

扩展:上面我们介绍了通过binlog_cahe_size 控制一个参数的大小。进行磁盘与内存缓存区的抉择。那么还有哪些参数是控制这一类问题的。你能想到几个?

下面我们延伸介绍一下sort_buffer_size ,从前几篇文章中引用的一段。详细的可以去order by那篇文章回顾一下。

binlog 写入机制大概就是这样了,以后技术的进步优化会再完善的。有不对的地方也可以加以指出!互相探讨学习!

redo log 写入机制

还是老样子吧。我感觉MySQL的设计很多地方都很相像。比如change buffer,binlog cache都是一些缓存区,内存临时存放的地方。那么redolog的缓冲区是什么呢? redo log buffer

我们可以先介绍一下 redo log buffer

redo log buffer 要做的是一个事务在插入一条数据的时候,需要先写入日志。但是又不能在还没有提交事务的时候直接写到redo log文件中。这个日志的临时存放处就是redo log buffer。真正在写入redo log文件的过程是在commit这一步完成的。

这里是开启一个事务下执行的。如果我们只执行一条insert 语句它又是如何实现的呢?

单独执行一个更新语句的时候,InnoDB 会自己启动一个事务,在语句执行完成的时候提交。过程跟上面是一样的,只不过是“压缩”到了一个语句里面完成。

上面我们说了 buffer是他的一个临时缓存区,那么 是不是所有buffe都要持久化到磁盘呢?

不需要 如果事务执行期间 MySQL 发生异常重启,那这部分日志就丢了。由于事务并没有提交,所以这时日志丢了也不会有损失。

那么事务还没提交的时候,redo log buffer 中的部分日志有没有可能被持久化到磁盘呢?

确实会有。这个问题,要从 redo log 可能存在的三种状态说起 如下图

  • redo log buffer:物理上这是MySQL的进程内存

  • FS page cache:写入到磁盘,但是还没有进行持久化。物理上是page cache文件系统。

  • hard disk,这个就是持久化到磁盘了。

图中的红色区域是内存操作,不涉及到磁盘IO。所以性能的非常快的。write也是非常快的,也就是图中的黄色部分。fsync的速度就慢了很多。因为持久化到磁盘。

MySQL没那么简单,这里的redo log是有一个写入策略的。我们下面介绍一下策略与案例。

写入策略这个就涉及到了一个参数innodb_flush_log_at_trx_commit 这个参数控制写入redo log,写入到磁盘的走向。为了提供更好的性能保障!

  • 设置为 0 的时候,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ;

  • 设置为 1 的时候,表示每次事务提交时都将 redo log 直接持久化到磁盘;

  • 设置为 2 的时候,表示每次事务提交时都只是把 redo log 写到 page cache。

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

下面我们可以细想一下,redo log buffer帮redo log解决了那么大 的一个难题。那么redo log buffer又是绝对的安全或者说绝对的性能吗?

如果我事务正在执行,还没有提交。那么MySQL肯定会把数据从redo log写入到redo log buffer!上面我们介绍了每隔一秒会把redo log buffer里的数据做一边写入。那么有没有可能事务没执行完,可能已经写盘了呢?

答案是肯定的。下面我们介绍一下redo log buffer的刷新策略

控制这个策略的参数是innodb_log_buffer_size

  • redo log buffer 占用的空间即将达到 innodb_log_buffer_size 一半的时候,后台线程会主动写盘。

(注意,由于这个事务并没有提交,所以这个写盘动作只是 write,而没有调用 fsync,也就是只留在了文件系统的 page cache。)

  • 另一种是,并行的事务提交的时候,顺带将这个事务的 redo log buffer 持久化到磁盘

(假设一个事务 A 执行到一半,已经写了一些 redo log 到 buffer 中,这时候有另外一个线程的事务 B 提交,如果 innodb_flush_log_at_trx_commit 设置的是 1,那么按照这个参数的逻辑,事务 B 要把 redo log buffer 里的日志全部持久化到磁盘。这时候,就会带上事务 A 在 redo log buffer 里的日志一起持久化到磁盘。)

说明: 如上图所示, 在做两阶段提交的时候会有一个prepare。先写入redolog处于prepare阶段。再写binlog。最后再commit。

假设: 如果innodb_log_buffer_size 设置成1,那么redo log在prepare就要持久化一次,因为有一个崩溃恢复的逻辑还要依赖prepare的redo log,再加上binlog来恢复。

每秒一次后台轮询刷盘,再加上崩溃恢复这个逻辑,InnoDB 就认为 redo log 在 commit 的时候就不需要 fsync 了,只会 write 到文件系统的 page cache 中就够了。

通常我们说 MySQL 的“双 1”配置,指的就是 sync_binlog 和 innodb_flush_log_at_trx_commit 都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare 阶段),一次是 binlog。

还远远不止这些,有些时候我们听说大厂的TPS每秒两万。也就是说每秒就会写四万次磁盘。但是,我用工具测试出来,磁盘能力也就两万左右,怎么能实现两万的 TPS? 组提交机制

虽然是最后一个模块,不过还是少不了概念

这里,先介绍日志逻辑序列号(log sequence number,LSN)的概念。LSN 是单调递增的,用来对应 redo log 的一个个写入点。每次写入长度为 length 的 redo log, LSN 的值就会加上 length。LSN 也会写到 InnoDB 的数据页中,来确保数据页不会被多次执行重复的 redo log。

后续会介绍一下lsn,redo log,checkpoint他们三者的区别。checkpoint这个还没忘记吧。就是前面介绍的刷内存的时候利用的就是这个checkpoint。redo log和LSN就是正在介绍的。

如下图所示,trx1,trx2,trx3三个并发事务在prepare阶段,都写完了redo log buffer持久化到磁盘的过程。

由图中可以得知

  • trx1是最先到达的,会被选为这组的leader。

  • 等 trx1 要开始写盘的时候,这个组里面已经有了三个事务,这时候 LSN 也变成了 160;

  • trx1 去写盘的时候,带的就是 LSN=160,因此等 trx1 返回时,所有 LSN 小于等于 160 的 redo log,都已经被持久化到磁盘;

  • 这时候 trx2 和 trx3 就可以直接返回了。

所以,一次组提交里面,组员越多,节约磁盘 IOPS 的效果越好。但如果只有单线程压测,那就只能老老实实地一个事务对应一次持久化操作了。

在并发更新场景下,第一个事务写完 redo log buffer 以后,接下来这个 fsync 越晚调用,组员可能越多,节约 IOPS 的效果就越好。

优化:

为了提升MySQL的性能,一般会选择在这个地方进行延迟,因为这样可以用节省更多的IOPS。

我们借助上文的两阶段提交的图!这里把写binlog日志这一过程分成了两步。

  • 先把 binlog 从 binlog cache 中写到磁盘上的 binlog 文件;

  • 调用 fsync 持久化。

根据这里的优化改一下。MySQL为了让组提交效果更好如下图。

这么一来,binlog也可以组提交了。为什么这么说呢。可以看上图的第二步。如果多个事务都已经write了(也就是说写入到redo log buffer了),再到第四步的时候就可以一起持久化到磁盘了。不是提升IOPS嘛的这个优化过程嘛!

不过通常情况下第 3 步执行得会很快,所以 binlog 的 write 和 fsync 间的间隔时间短,导致能集合到一起持久化的 binlog 比较少,因此 binlog 的组提交的效果通常不如 redo log 的效果那么好。

如果你想提升 binlog 组提交的效果,可以通过设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 来实现。这两个只要有一个满足条件就会调用 fsync。

  • binlog_group_commit_sync_delay 参数,表示延迟多少微秒后才调用 fsync;

  • binlog_group_commit_sync_no_delay_count 参数,表示累积多少次以后才调用 fsync。

所以,当 binlog_group_commit_sync_delay 设置为 0 的时候,binlog_group_commit_sync_no_delay_count 也无效了。

回到上文的WAL机制那里我们继续讨论一下。WAL机制主要得以于两方面

  • redo log 和 binlog 都是顺序写,磁盘的顺序写比随机写速度要快;

  • 组提交机制,可以大幅度降低磁盘的 IOPS 消耗。

实战案例

如果你的 MySQL 现在出现了IO性能瓶颈,可以通过哪些方法来提升性能呢?

  • 设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 参数,减少 binlog 的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。

  • 将 sync_binlog 设置为大于 1 的值(比较常见是 100~1000)。这样做的风险是,主机掉电时会丢 binlog 日志。

  • 将 innodb_flush_log_at_trx_commit 设置为 2。这样做的风险是,主机掉电的时候会丢数据。

我不建议你把 innodb_flush_log_at_trx_commit 设置成 0。因为把这个参数设置成 0,表示 redo log 只保存在内存中,这样的话 MySQL 本身异常重启也会丢数据,风险太大。而 redo log 写到文件系统的 page cache 的速度也是很快的,所以将这个参数设置成 2 跟设置成 0 其实性能差不多,但这样做 MySQL 异常重启时就不会丢数据了,相比之下风险会更小。

总结

今天我们已经介绍完了两大日志的写入机制。以及日志的两阶段提交的优缺点。组提交机制的流程与性能


浏览 15
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报