Redis 持久化机制

Redis 是一个内存数据库,数据保存在内存中。众所周知,内存中的数据变化快且易丢失,因此 Redis 提供了两种持久化机制:快照(RDB)追加日志(AOF)

注意:Redis 4.0 版本新增了混合持久化机制,将 RDB 的内容和增量 AOF 放在一起。这里的 AOF 记录的是从 RDB 生成开始到结束之间的增量日志。

快照持久化(RDB)

执行快照操作时,Redis 必须进行 I/O 读写操作。文件 I/O 操作可能会严重影响服务器请求的性能。此外,如果在保存过程中数据发生修改或删除,可能会导致数据不一致。为了解决这些问题,Redis 使用了 COW 机制(Copy-On-Write,写时复制) 来实现持久化。

COW 实现原理

fork() 之后,Kernel 把父进程中所有的内存页的权限都设为 read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。当其中某个进程写内存时,CPU 硬件检测到内存页是 read-only 的,于是触发页异常中断(page-fault),陷入 Kernel 的一个中断例程。中断例程中,Kernel 就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。

COW 的优缺点

优点:

  1. 可减少分配和复制大量资源时带来的瞬间延时。
  2. 可减少不必要的资源分配。例如 fork 进程时,并不是所有的页面都需要复制,父进程的代码段和只读数据段都不被允许修改,所以无需复制。

缺点:

  1. 如果在 fork() 之后,父子进程都还需要继续进行写操作,那么会产生大量的分页错误(页异常中断 page-fault,需要将触发的异常的内存页复制一份),这样就得不偿失。

Redis 中的 COW 应用

Redis 在持久化时,如果是采用 BGSAVE 命令或者 BGREWRITEAOF 的方式,那 Redis 会 fork 出一个子进程来读取数据,从而写到磁盘中。

总体来看,Redis 还是读操作比较多。如果子进程存在期间,发生了大量的写操作,那可能就会出现很多的分页错误(页异常中断 page-fault),这样就得耗费不少性能在复制上。

而在 rehash 阶段上,写操作是无法避免的。所以 Redis 在 fork 出子进程之后,将负载因子阈值提高,尽量减少写操作,避免不必要的内存写入操作,最大限度地节约内存。

追加日志持久化(AOF)

AOF 日志存储的是 Redis 服务器的顺序指令序列,只记录对内存进行修改的指令记录。持久化功能的实现可以分为 命令追加文件写入文件同步 三个步骤。

命令追加

当 AOF 持久化功能打开时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的 aof_buf 缓冲区的末尾。

AOF 文件的写入与同步

每当服务器常规任务函数被执行、或者事件处理器被执行时,flushAppendOnlyFile 函数都会被调用。这个函数执行以下两个工作:

  1. WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。
  2. SAVE:根据条件,调用 fsyncfdatasync 函数,将 AOF 文件保存到磁盘中。

两个步骤都需要根据一定的条件来执行,而这些条件由 AOF 所使用的保存模式来决定。Redis 目前支持三种 AOF 保存模式:

  • AOF_FSYNC_NO:不保存。
  • AOF_FSYNC_EVERYSEC:每一秒钟保存一次。
  • AOF_FSYNC_ALWAYS:每执行一个命令保存一次。

第一种模式(NO)

每次调用 flushAppendOnlyFile 函数,WRITE 都会被执行,但 SAVE 会被略过。以下三种场景的 SAVE 操作都会引起 Redis 主进程阻塞:

  1. Redis 被关闭。
  2. AOF 功能被关闭。
  3. 系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行)。

第二种模式(EVERYSEC)

SAVE 原则上每隔一秒钟就会执行一次。因为 SAVE 操作是由后台子线程调用的,所以它不会引起服务器主进程阻塞。但是在 flushAppendOnlyFile 函数被调用后会出现四种情况:

在这里插入图片描述

第三种模式(ALWAYS)

每次执行完一个命令之后,WRITESAVE 都会被执行。另外,因为 SAVE 是由 Redis 主进程执行的,所以在 SAVE 执行期间,主进程会被阻塞,不能接受命令请求。

AOF 文件的读取和数据还原

AOF 文件保存了 Redis 的数据库状态,而文件里面包含的都是符合 Redis 通讯协议格式的命令文本。这也就是说,只要根据 AOF 文件里的协议,重新执行一遍里面指示的所有命令,就可以还原 Redis 的数据库状态了。

Redis 读取 AOF 文件并还原数据库的详细步骤如下:

  1. 创建一个不带网络连接的伪客户端(fake client)。
  2. 读取 AOF 所保存的文本,并根据内容还原出命令、命令的参数以及命令的个数。
  3. 根据命令、命令的参数和命令的个数,使用伪客户端执行该命令。
  4. 执行步骤 2 和 3,直到 AOF 文件中的所有命令执行完毕。

完成第 4 步之后,AOF 文件所保存的数据库就会被完整地还原出来。

注意:因为 Redis 的命令只能在客户端的上下文中被执行,而 AOF 还原时所使用的命令来自于 AOF 文件,而不是网络,所以程序使用了一个没有网络连接的伪客户端来执行命令。伪客户端执行命令的效果,和带网络连接的客户端执行命令的效果完全一样。

AOF 重写

Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,因此需要对 AOF 进行重写。

Redis 提供了 BGREWRITEAOF 指令用于对 AOF 日志进行瘦身。其原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件。

使用子进程也有一个问题需要解决:AOF 重写期间如果有新的写命令进来,不能漏掉,否则会导致数据不一致。于是 Redis 服务器设置了一个 AOF 重写缓冲区,最后流程变为:

  1. 执行客户端发来的命令。
  2. 将执行的写命令追加到 AOF 缓冲区。
  3. 将执行后的写命令追加到 AOF 重写缓冲区。

这样一来可以保证:

  1. AOF 缓冲区的内容会定期被写入和同步到 AOF 文件,对现有 AOF 文件的处理工作会照常进行。
  2. 从创建子进程开始,服务器执行的所有写命令会被记录到 AOF 重写缓冲区里面。
  3. 当子进程完成 AOF 重写工作之后,它会向父进程发送一个信号,父进程收到信号后,会调用一个信号处理函数,并执行以下工作:

    1. 将 AOF 重写缓冲区中的所有内容写入新的 AOF 文件中,这时新 AOF 文件所保存的数据库状态和服务器当前状态一致。
    2. 对新的 AOF 文件进行改名,原子性操作地覆盖现有的 AOF 文件,完成新旧 AOF 文件的替换。

这个信号函数执行完毕以后,父进程就可以继续像往常一样接受命令请求了。在整个 AOF 后台重写过程中,只有信号处理函数执行时会对服务器进程造成阻塞,其他时候都可以继续处理请求,这样 AOF 重写对服务器性能造成的影响降到了最低。

说明:本文内容基于 Redis 核心机制整理,其中混合持久化特性适用于 Redis 4.0 及以上版本。