被写入 AOF 文件的所有命令都是以 Redis 的命令请求协议格式保存的,这是一种纯文本格式,可以直接打开观察其内容。例如,下图是在 0 号数据库中执行了 SET、SADD 和 RPUSH 命令后保存的 AOF 文件内容。
其中,除了用于指定数据库的 SELECT 命令是服务器自动添加的之外,其他的都是之前通过客户端发送的命令。
AOF 持久化功能的实现可以分为命令追加、文件写入和文件同步三个步骤。
当 AOF 持久化功能打开时,服务器在执行完一个写命令后,就会以协议格式将这个命令追加到服务器状态的 aof_buf 缓冲区中。
struct redisServer{ // ... sds aof_buf; // AOF 缓冲区 // ... };
Redis 的服务器进程就是一个事件循环,其中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像 serverCron 函数这样需要定时运行的函数。因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到 aof_buf 缓冲区里面,所以服务器每结束一个事件循环之前,都会调用 flushAppendOnlyFile 函数,以考虑是否需要将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件里面。这个过程可以使用以下伪代码表示。
def eventLoop(): while True: processFileEvents() # 处理文件事件,接收命令请求以及发送命令回复 processTimeEvents() # 处理时间事件 flushAppendOnlyFile() # 考虑是否要将 aof_buf 中的内容保存到 AOF 文件
flushAppendOnlyFile 函数的行为由服务器配置的 appendfsync 选项的值(默认为 everysec)来决定,各个不同值产生的行为如下表所示。
其中:
当 appendfsync 的值为 always 时,服务器在每个事件循环都要将 aof_buf 缓冲区的内容写入并等待同步到 AOF 文件,所以效率是最慢的。但从安全性来说,它却是最安全的,因为即使出现故障停机,AOF 持久化也只会丢失一个事件循环中所产生的命令数据。
当 appendfsync 的值为 everysec 时,服务器在每个事件循环都要将 aof_buf 缓冲区中的内容写入到 AOF 文件,但每隔一秒才在子线程中对 AOF 文件进行一次同步,因此这种方式最多丢失一秒钟的命令数据。
当 appendfsync 的值为 no 时,服务器同样在每个事件循环都要将 aof_buf 缓冲区中的内容写入到 AOF 文件,不过至于何时对 AOF 文件进行同步,则由操作系统自行控制。因此这种方式的 AOF 写入速度通常是最快的,但由于会在系统缓存中积累一段时间的写入数据,所以其单次同步时长通常是最长的,它最多丢失上次同步 AOF 文件之后的所有写命令数据。
由于 AOF 文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍 AOF 文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。Redis 读取 AOF 文件并还原数据库状态的流程如下。
注意,这里之所以创建一个不带网络连接的伪客户端(fake client),是因为 Redis 的命令只能在客户端上下文中执行,而载入 AOF 文件时所用的命令直接来源于 AOF 文件而不是网络连接,所以才创建一个伪客户端来达到普通客户端执行命令的效果。
根据上面所述易知,AOF 文件的体积会随着服务器运行时间的流逝而变得越来越大,如果不加以控制,可能会对 Redis 服务器、甚至整个宿主机造成影响,而且 AOF 文件的体积越大,也表示使用 AOF 文件来进行数据还原所需的时间也就越多。
为了解决 AOF 文件体积膨胀过快的问题,Redis 提供了 AOF 文件重写功能。不过虽然这个功能被称为“AOF 文件重写”,但实际上它并不需要对现有的 AOF 文件进行任何的读取、分析或者写入操作,而是通过直接读取数据库中的键值对,并创建一个新的 AOF 文件替代现有的 AOF 文件来实现的。新旧两个 AOF 文件所保存的数据库状态相同,但新 AOF 文件不会包含冗余命令(比如,对于多个连续的 LPOP 列表操作,无需记录多条 LPOP 命令操作,而只需用一条命令记录列表的最终状态即可),所以其体积往往比旧 AOF 文件的要小得多。
要注意的是,在实际中,为了避免执行命令时造成客户端输入缓冲区溢出,重写程序在处理列表、哈希表、集合和有序集合这四种可能含有多个元素的键时,会先检查键所包含的元素数量,如果超过了 redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD 常量(一般为 64)的值,那么重写程序将使用多条命令来记录键的值,而不单单使用一条命令。
最后,因为 AOF 重写程序通常会进行大量的写入操作,所以为了避免阻塞服务器,Redis 会将其放到子进程里执行。不过虽然使用子进程而非线程可以在避免使用锁的情况下保证数据的安全性,但这却需要解决一个问题,即子进程在进行 AOF 重写期间,服务器进程(父进程)接收到的新的命令可能会修改现有的数据库状态,从而使得与子进程利用父进程的数据副本重写后的 AOF 文件所保存的数据库状态不一致。因此,为了解决这种数据不一致问题,Redis 服务器设置了一个 AOF 重写缓冲区,它会在服务器创建子进程之后开始使用:当 Redis 服务器执行完一个写命令后,会同时将这个写命令追加到 AOF 缓冲区和 AOF 重写缓冲区。
当子进程完成 AOF 重写工作后,会向父进程发送一个信号,父进程则会在相应的信号处理函数中执行以下工作:
1)将 AOF 重写缓冲区中的所有内容写入到 AOF 文件中,这时 AOF 文件所保存的数据库状态将和服务器当前的数据库状态一致。
2)对新的 AOF 文件进行改名,原子地覆盖现有的 AOF 文件,完成新旧两个 AOF 文件的替换。
这就是 AOF 后台重写,也即是 BGREWRITEAOF 命令的实现原理。
参考书籍:《Redis 设计与实现》第 11 章——AOF 持久化。