AOF持久化
AOF持久化是通过保存redis服务器所执行的写命令来记录数据库状态的。

写入到AOF文件的命令都是以redis的命令请求格式保存的,且redis的命令请求是纯文本格式,四亿可以直接打开AOF文件查看内容。
具体实现
AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤。
命令追加
当AOF持久化功能处于开启状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾:
1 | struct redisServer { |
写入和同步
redis的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。
在处理文件事件的可能会执行写命令,使得一些内容被追加到aof_buf缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,判断是否需要将aof_buf缓冲区的内容写入和保存到AOF文件中,用一段伪代码表示这一过程。
1 | void eventLoop(){ |
flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定,各个不同值产生的行为如下
appendfsync选项的值 |
flushAppendOnlyFile函数的行为 |
|---|---|
always |
将aof_buf缓冲区中的所有内容写入并同步到AOF文件中 |
everysec(默认值) |
将aof_buf缓冲区中的所有内容写入到AOF文件中,如果上一次同步AOF文件的时间距离现在超过一秒钟,那么再次对AOF文件进行同步,并且这个同步操作事由一个线程专门负责执行的。 |
no |
将aof_buf缓冲区中的所有内容写入到AOF文件中,但是并不对AOF文件进行同步,何时同步由操作系统来决定。 |
如果用户没有主动为appendfsync选项设置值,那么appendfsync选项的默认值为everysec,关于appendfsync选项的更多信息,请参考Redis项目附带的示例配置文件redis.conf。
如何选择:
- 当
appendfsync的值为always时,服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件,并且同步AOF文件,所以always的效率是appendfsync选项三个值当中最慢的一个,但从安全性来说,always也是最安全的,因为即使出现故障停机,AOF持久化也只会丢失一个事件循环中所产生的命令数据。 - 当
appendfsync的值为everysec时,服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件,并且每隔一秒就要在子线程中对AOF文件进行一次同步。从效率上来讲,everysec模式足够快,并且就算出现故障停机,数据库也只丢失一秒钟的命令数据。 - 当
appendfsync的值为no时,服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件,至于何时对AOF文件进行同步,则由操作系统控制。因为处于no模式下的flushAppendOnlyFile调用无须执行同步操作,所以该模式下的AOF文件写入速度总是最快的,不过因为这种模式会在系统缓存中积累一段时间的写入数据,所以该模式的单次同步时长通常是三种模式中时间最长的。从平摊操作的角度来看,no模式和everysec模式的效率类似,当出现故障停机时,使用no模式的服务器将丢失上次同步AOF文件之后的所有写命令数据。
文件载入与还原
因为AOF文件里面包含了重建数据库状态的所有命令,所以服务器只要读入并重新执行一边AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库。还原步骤大致如下:
- 创建一个不带网络连接的伪客户端(fake client):因为redis的命令只能在客户端上下文中执行,而载入AOF文件时使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的客户端来执行AOF文件所保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令效果是一样的。
- 从AOF文件中分析并读取一条写命令
- 使用伪客户端执行被读出的写命令
- 一直执行步骤2、3,直到AOF文件中的命令都被处理完毕。

AOF重写
从上述表达的内容来看,会暴露出一个问题:随着服务器的运行,AOF文件记录的内容也会越来越多,文件体积也会越来越大,如果不加以控制,并且AOF文件的体积越大,使用AOF文件来进行数据还原所需的时间就越多。
举个例子:
1 | rpush list 'a' 'b' |
当客户端执行了上述6条命令,AOF文件就会依次记录这6条命令。在业务中,这类写操作会很频繁,也就导致了AOF的体积不断膨胀。
为了解决AOF文件体积不断膨胀的问题,redis提供了AOF文件重写(rewrite)功能。通过该功能,redis服务器可以创建一个新的AOF文件来代替现在的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但是新的AOF文件不会包含任何冗余的命令,所以新的AOF文件的体积会比原先小得多。
实现:
🚨注意:redis重写后的生成的新AOF文件替换掉旧的AOF文件。新的AOF并非通过对现有AOF文件进行读取分析或者其他操作产生的,而是通过读取服务器当前的数据库状态来实现的。
好比上述例子中,旧的AOF文件记录了6条数据,重写之后的AOF文件只保留了rpush list 'c' 'd' 'e' 'f' 'g'这么一条命令。
书中对整个重写过程的伪代码解析:
1 | def aof_rewrite(new_aof_file_name): |
因为aof_rewirte函数生成的新AOF文件只包含还远当前数据库状态所必须的命令,所以新AOF文件不会浪费任何磁盘空间。
⚠️注意:在实际中,为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序在处理列表、哈希表、集合、有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数量,如果元素数量超过了**AOF_REWRITE_ITMES_PER_CMD**常量的值,那么重写程序将使用多条命令来记录键的值,而不单单是用一条命令。
简单说名义下,【 关于AOF的功能都写在了
aof.c文件中,其中**AOF_REWRITE_ITMES_PER_CMD常量定义在server.h文件中,值为64**。这里与《Redis设计与实现》这本书中的描述有所差别,原文是redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD,该书作者在书中提到,是基于Redis 2.9来编写。而笔者所查看的redis源码是6.0.6版本,故而有所差异。
后台重写
从上述中可以了解到,AOF重写程序可以很好的创建一个新AOF文件的任务,但是因为这个函数会进行大量的写操作,所有调用这个函数的线程将被长时间阻塞,而且redis服务器是使用单线程来处理命令请求的。所以为了在重写AOF文件期间,不影响redis服务器处理命令,redis将AOF重写放在子进程里执行,如此达到两个目的:
- 子进程进行AOF重写期间,服务器进程可以继续处理命令
- 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性。
这样的话,会产生另一个问题:子进程在进行AOF期间,服务器进程还在处理新的命令请求,这回导致重写后的AOF文件保存的数据库状态与当前数据库状态不一致。
为解决数据不一致问题:redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当redis服务器执行完一个命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区。

如此一来,子进程执行AOF重写期间,服务器进行需要执行一下三个工作:
- 执行客户端发送的命令
- 将执行后的写命令追加到AOF缓冲区
- 将执行后的写命令追加到AOF重写缓冲区
这样即可保证:
- AOF缓冲区的内容会定期被写入和同步到AOF文件,对现有的AOF文件的处理工作正常进行。
- 从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区。
当子进程完成了AOF重写工作之后,它会向父进程发送一个信号,父进程接收到信号之后,会调用处理信号的函数,并执行以下工作:
- 将AOF重写缓冲区中的所有内容写入到新AOF文件中,这时新AOF文件所保留的数据库状态将和服务器当前的数据库状态一致。
- 对新的AOF文件进行改名,原子的覆盖现有的AOF文件,完成新旧两个AOF文件的替换。
等这个信号处理函数执行完毕之后,父进程就可以继续执行命令请求。
在整个AOF后台重写过程中,只有信号处理函数执行时会对服务器进程(父进程)造成阻塞,在其他时候,AOF后台重写都不会阻塞父进程,这将AOF重写对服务器性能造成的影响降到了最低。