Redis 的持久化机制

讨论 Redis 持久化机制之前,我们应该先想一下 MySQL 是怎么做的数据持久化:先把数据保存到日志当中,再执行 SQL 语句,这样就保证了 MySQL 突然宕机后的数据恢复能力,直接读取日志即可恢复。

那么对于 Redis 来讲,也是一样的操作:写日志。Redis 中提供了两种持久化策略,一种是 RDB 持久化、一种是 AOF 持久化。

RDB 持久化

RDB 持久化本质上保存的是 数据快照,它把 Redis 中保存的数据以二进制的形式写入到一个 RDB 文件当中,该文件默认是 dump.rdb

需要注意的是 RDB 持久化保存的是当前时间下 Redis 内的二进制全量数据,这就会引发一个问题:RDB 持久化每次都保存 Redis 的全量数据,那这个过程是不是很慢?

答案是肯定的。而 Redis 提供了两个命令来实现 RDB 的持久化:savebgsave

先来看 save :save 是在主线程去保存 RDB 数据快照,这样势必就会带来一个问题:阻塞主进程的读写命令执行,所以这种方式是我们所不推荐的。

再来看 bgsave :bgsave 表示开启一个子进程来执行 RDB 持久化,这样就不会阻止主进程读写的命令了,这是 RDB 持久化的默认选择。实际上这种方式的持久化机制使用了一种 “写时复制” 的思想,在 JDK 中的体现就是 CopyOnWriteArraylist 类。

但是即使是使用 bgsave 开启一个子进程来执行 RDB 持久化,不还是存在 RDB 持久化过程缓慢的缺点吗?

是的,这是不可避免的。而由于 RDB 过程比较慢,我们最好手动设置一个持久化频率来避免频繁地进行 RDB 持久化。通过配置文件可以配置在 x 秒内有 y 个 key 发生变化就进行 RDB 持久化操作,或者直接固定好时间,比如 5 分钟执行一次持久化。但是如果在这个过程中 Redis 出现了宕机,那么就会相应的丢失规定时间内的数据。

这就是 RDB 持久化的缺点:过程缓慢且容易丢失数据。但 RDB 持久化也是具有一定的优势的:RDB 内存储的是二进制的全量数据,Redis 故障恢复后直接读取数据写入内存就能恢复,也就是说,RDB 持久化方式下 Redis 的故障恢复能力很快

在这里就又引出了一个问题:子进程保存 RDB 快照时,主进程还能写入或者修改数据吗?

答案是可以的。上面我们提到 RDB 的 bgsave 方式是基于 “写时复制” 的思想,这样就保证了 RDB 持久化的过程中也是可以在主线程进行数据的写入或修改操作的

主进程 fork 子进程后,并不是把所有数据都复制一份给子进程,而是使主进程与子进程共享相同的内存页面。也就是说仅仅复制了页表,使二者指向同一个物理地址,这样可以加快 fork 的速度,减少性能消耗。

img

如果此时收到了写命令,那么主进程会对数据所在的页进行复制一份,在副本上进行修改,此时子进程还是指向的老的页,数据是没有发生变化的,这就是 写时复制 的思想。

img

AOF 持久化

RDB 是保存二进制全量数据进行持久化,那我们可不可以试想一下另一种方案:不保存全部的数据了,只保存写命令的操作。即使 Redis 宕机了,那等 Redis 重启之后再次执行一遍这些写命令,不也是可以恢复数据吗,这就是 AOF 的思想

AOF 持久化机制是什么样的?我们不妨先想想 MySQL 的思路:先把数据写入到 Buffer Pool 缓冲区,然后在某个时间段异步的刷入到磁盘当中。所以对于 Redis 的 AOF 来讲,过程是类似的:

  • 先执行主线程的命令
  • 然后记录命令到 AOF 缓冲区
  • 其次写入操作系统的内核缓冲区 Page Cache
  • 最后在某个时间段内刷入到磁盘当中

那么在 AOF 中到底什么时间才会把数据刷入磁盘呢,Redis 给我们提供了三种选择:alwaysnoeverysec

先来看 always :这种方式是通过主线程每次执行完命令就立刻刷入到磁盘当中,该方式能够最大程度的保证数据的不丢失,但是是由主线程来操作的,会对主线程执行命令时的性能造成一定影响。

再来看 no :这种方式是说不主动进行刷盘,而是由操作系统自己来决定什么时候把数据刷入磁盘。这种方式虽然缓解了主线程的性能影响,但是其刷盘时机是不确定的,会一定程度上增加数据丢失的风险。

最后看 everysec :该方式是指每次执行完命令后,先把命令写入到缓冲区,然后每隔 1 秒刷一次盘,这是对前面两种方式的一种折中选择。在该方式下,即使丢失数据也只会丢失 1 秒内的数据,既保证了 Redis 执行命令的性能,又不会丢失太多的数据。

下面我们来看两个问题:

1、always 一定能够保证不丢失数据吗?

always 也不能保证数据一定不丢失。因为 Redis 是先执行命令再写入 AOF,如果写入 AOF 这段时间宕机,那么 AOF 就不能保证数据的存在。

2、为什么 AOF 先执行命令再写入日志,而不是像 MySQL 先写入日志,再执行命令?

从 MySQL 的角度来看:MySQL 是关系型数据库,其根本的工作最大程度上就是保证数据不能丢失。其核心是保证事务的 ACID,保证数据的持久性,数据是坚决不能丢的,因此选择先写入日志,再执行命令。

而对于 Redis 来说,虽然也是数据库,但往往 Redis 的应用场景是作为缓存存在,也就是说是作为临时存储数据的桥梁,实际数据还是以 MySQL 中的数据为主。而我们用缓存最大的目的不就是 嘛。所以 Redis 先执行命令再写入日志 (写入 AOF 可以理解为写日志),完全符合其 的特点。

换一个角度再来看:Redis 先执行命令,那么就说明了 Redis 执行的命令是有效的,这样在执行 AOF 的时候就不需要再进行命令合法性的检查了,可以直接写入。

假设有如下的场景:set key1 1set key1 2set key1 3 ……、set key1 99999 ,对于这样的一串命令,实际上只有最后一次的命令是有效的,前面的命令都是历史值,我们不关心。

但是 AOF 是会记录所有的写命令的啊,这样一来不就导致了 AOF 文件过大,且 Redis 宕机重启后会执行前面一堆的无效命令?

针对于上述情况,AOF 提供了重写机制

AOF 重写会根据现有的 AOF 文件进行重写,意思就是:新建一个新的 AOF 文件用于写入符合要求的有效的命令,形成一个与原 AOF 文件等效的文件,且体积还比原 AOF 文件小。AOF 重写是 Redis 开启一个新进程实现的。

总结

RDB 是全量数据快照,保存慢,需要每隔一段时间保存一次且容易丢数据,但是 Redis 重启恢复数据比较快。

AOF 仅保存 Redis 的写命令,保存快且不容易丢数据,但是 Redis 重启后恢复数据需要一条条执行命令,这个过程比较慢。

RDB 与 AOF 混合持久化

有没有一种方案能够兼顾 RDB 的故障恢复能力且 AOF 的不易丢失数据的特点呢?

Redis 4.0 版本提出了 RDB 与 AOF 混合持久化 的方案来实现了二者优点的兼顾,其工作原理是:

1、在执行 AOF 重写的时候,把当前 Redis 中的数据以 RDB 的方式写入 AOF,把写命令以 AOF 的方式写入 AOF。

2、这样就形成了重写后的 AOF 文件,前半段是 RDB 的二进制文件,后半段是 AOF 记录的 Redis 写命令。

3、这样就保证了 Redis 故障恢复能力的速度,还降低了数据丢失的风险。