Redis 持久化机制

我们知道 Redis 之所以那么快,是因为它是内存数据库,将所有数据都存在内存中。相比于 MySQL 来说,Redis 减少了每次数据需要从磁盘读入到内存中的 IO 开销

可是一旦 Redis 服务重启,那么内存中的数据就会被清空,导致数据丢失。为了保留数据,Redis 支持三种持久化方案:

RDB

RDB 通过创建快照 (dump.rdb 文件) 来获取 Redis 数据库在某个时间点上的副本;当 Redis 重启时可以通过加载快照文件 (dump.rdb) 来恢复上次内存中的数据

有两个 Redis 命令可以生成 RDB 文件:

Redis 默认使用 RDB 持久化机制,且默认选择bgsave命令。Redis 的配置文件redis.conf中如下配置:

只要满足上述三个条件中的任意一个,bgsave命令就会被执行

这里先说明一下子进程和父进程的内存关系。假设父进程内存中有一个变量a,那么子进程内存中也应该有一个变量a

最开始父子进程都指向内存中同一个a,一旦有进程对a修改时,就会复制一个新的a,此时父子进程指向不再是同一个a,这就是写时复制技术 (copyOnWrite)

再衍生一下,如果各自输出a的内存地址会发现是一样的,这是因为输出的内存地址其实是相对于进程的偏移量,并非实际的物理内存地址。由于是父子进程,所以a在两个进程中的偏移量是一样的

这里给出一个程序:

回归主题,下面给出bgsave命令执行的流程图:

2

最后再来说一下 RDB 的优缺点。先说优点

再来说缺点

AOF

如果说 RDB 是保存某个时间点的数据库状态 (快照),那么 AOF 就是保存导致数据库状态的过程,通过重新执行一遍过程使数据库到达目标状态

更官方一点:AOF 持久化通过保存 Redis 服务器所执行的增删改命令来记录数据库状态,重新执行保存命令即可恢复数据

与 RDB 相比,AOF 的实时性更好。假设某个时间段内仅有一条命令使数据库改变,那么 RDB 需要重新生成一份数据库快照,而 AOF 只需要记录该条修改命令即可

默认情况下 Redis 并没有开启 AOF,如果要开启,需要修改配置文件appendonly yes,默认文件名为appendonly.aof

AOF 的工作流程主要有四个步骤:命令写入 (append)、文件同步 (sync)、文件重写 (rewrite)、重启加载 (load)

3

命令写入 (append)

Redis 服务器可以解析 RESP 标准的命令,每次客户端发送命令到服务器也是将其封装成 RESP 格式,具体格式如下:(CRLF 表示\r\n)

对于set hello world命令,它在 RESP 标准下的格式为:

而 AOF 也是基于 RESP 标准存储命令

AOF 并没有直接把命令写入到文件中,而是先写到 AOF 缓冲区 aof_buf 中,至于缓冲区何时真正同步到文件中取决于采取的策略,这也是下个步骤要做的事情

文件同步 (sync)

文件同步是将 AOF 缓冲区的内容同步到文件中,这一步才算真正的将内存数据持久化。Redis 提供了多种 AOF 缓冲区同步文件策略,由参数appendfsync控制,不同值的含义如下表:

appendfsync 选项的值行为
always主线程调用 write 把命令写入 aof_buf 缓冲区
后台线程 (aof_fsync 线程) 立即调用 fsync 同步到 AOF 文件 (刷盘),fsync 完成后线程返回
everysec主线程调用 write 把命令写入 aof_buf 缓冲区
后台线程每秒调用一次 fsync 同步到 AOF 文件 (刷盘)
no主线程调用 write 把命令写入 aof_buf 缓冲区
刷盘操作由操作系统决定,通常同步周期最长 30s

关于系统调用writefsync的说明:

如果选择always同步策略,将牺牲效率换取安全性;如果选择no同步策略,将牺牲安全性换取效率;所以一般来说选择everysec同步策略较好

文件重写 (rewrite)

随着 Redis 服务器的运行,AOF 文件只会越来越大,而且还可能会存在很多冗余命令,比如服务器执行了下面两条命令:

由于这两条命令对数据库进行了修改,所以会将它们存入 AOF 文件中,待重启加载时执行。如果将这两条指令看作一个整体,相当于没有对数据库进行修改 (添加后删除)

所以文件重写就是新创建一个 AOF 文件,然后交由子进程去遍历数据库中的键值对,根据数据库状态生成最简执行命令,最后将新 AOF 文件替换旧 AOF

AOF 重写过程可以通过调用bgrewriteaof命令手动触发,也可以自动触发,满足以下两个条件时自动触发

由于文件重写是fork()出来的子进程去完成,而父进程依旧可以处理客户端命令,所以新创建出来的 AOF 文件和实际的数据库存在一个状态差

4

Redis 设置了一个 AOF 重写缓冲区,在文件重写操作触发后,将 Redis 服务器执行的命令同时追加到 AOF 缓冲区 (aof_buf) 和 AOF 重写缓冲区 (aof_rewrite_buf) 中

当子进程完成文件重写任务后,会向父进程发送一个信号,父进程接收到该信号后会进行两个操作:(这两个操作会阻塞父进程,也就是 Redis 服务进程)

下面给出文件重写完整的流程图:

5

这里再强调一遍:父进程把重写期间的增删改命令缓存到 aof_rewrite_buf,等重写完成后由父进程追加到新 AOF 文件中,追加过程是阻塞滴!!

如果重写期间父进程执行的命令较多,那么 aof_rewrite_buf 中会积攒大量的命令,而等子进程重写完成后会由父进程阻塞式将 aof_rewrite_buf 追加到新 AOF 中,会导致客户端的长时间得不到响应

为了改善上面的问题,Redis 通过在父子进程间建立管道,在子进程重写的后期阶段,父进程会将 aof_rewrite_buf 中积攒的命令通过管道发送给子进程,由子进程将这些数据追加到新 AOF 文件中

可能由于 aof_rewrite_buf 命令过多,导致子进程无法全部消费完,最后 aof_rewrite_buf 中剩余部分再由父进程阻塞式的追加到新 AOF 中。此时的 aof_rewrite_buf 相比于最初的小很多了

利用管道优化 AOFRW 更详细分析可见 Redis · 原理介绍 · 利用管道优化aofrewrite

你以为到这里就完了吗??其实并没有,优化过后的重写操作依旧存在一些问题:

阿里开发者团队在 Redis7.0 中发布了 Multi part AOF,详细可见 Redis 7.0 Multi Part AOF的设计和实现

从名字可以看出,Multi part AOF 就是将原来的单个 AOF 文件拆分成多个 AOF 文件。在 MP-AOF 中,将 AOF 分为三种类型:

为了管理这些 AOF 文件,引入了一个 manifest (清单) 文件来跟踪、管理这些 AOF 文件

下面给出 MP-AOF 完整的流程图:

6

从图中可以看出,在 AOFRW 期间不再需要 aof_rewrite_buf,省去了对应的内存消耗;父子进程之间也不需要传输数据和控制交互,省去了对应的 CPU 和磁盘 IO 开销

重启加载 (load)

每次 Redis 服务启动时,它的加载流程如下:

7

RDB + AOF 混合持久化

Redis 4.0 开始支持 RDB 和 AOF 混合持久化,默认关闭,可以通过配置aof-use-rdb-preamble yes开启

由于 AOF 文件重写时,子线程会遍历数据库,生成一个新 AOF 文件,而 RDB 正好是数据库的快照。所以不谋而合,直接将 RDB 写到 AOF 文件开头,形成[RDB file][AOF tail]

好处:快速加载避免丢失过多的数据

缺点:AOP 文件格式可读性差

RDB 和 AOF 比较

关于 RDB 和 AOF 的优缺点,官网也给了详细的说明 Redis persistence

RDB 比 AOF 优秀的地方

AOF 比 RDB 优秀的地方

综上所述

RDB 和 AOF 对过期键的处理

RDB 生成:在执行savebgsave命令创建一个新的 RDB 文件时,会对数据库中的键进行检查,已过期的键不会保存到新创建的 RDB 文件中

RDB 载入:如果是主服务器载入,会对键进程检查,不会载入已过期的键;如果是从服务器载入,无论是否过期,都会载入,因为主从同步时会清空从服务器

AOF 写入:如果数据库中某个键已经过期,但还没有被惰性删除或定期删除,那么对 AOF 文件不会有任何影响,当过期键被惰性删除或定期删除后,会向 AOF 中追加del命令

AOF 重写:在重写过程中,会对数据库的键进行检查,已过期的键不会被保存到重写后的 AOF 文件中

参考文章