源码位置:aof.c/rio.c/rio.h
1. 前言
除了RDB持久化功能以外,Redis还提供了AOF(Append Only File)持久化功能。与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis所执行的写命令来记录数据库状态的。
2. RDB和AOF的区别
antirez 在《Redis 持久化解密》一文中讲述了 RDB 和 AOF 各自的优缺点:
- RDB 是一个紧凑压缩的二进制文件,代表 Redis 在某个时间点上的数据备份。非常适合备份,全量复制等场景。比如每6小时执行 bgsave 备份,并把 RDB 文件拷贝到远程机器或者文件系统中,用于灾难恢复。
- Redis 加载 RDB 恢复数据远远快于 AOF 的方式
- RDB 方式数据没办法做到实时持久化,而 AOF 方式可以做到。
3. AOF持久化的实现
如上图所示,AOF 持久化功能的实现可以分为命令追加( append )、文件写入( write )、文件同步( sync )、文件重写(rewrite)和重启加载(load)。其流程如下:
- 所有的写命令会追加到 AOF 缓冲中。
- AOF 缓冲区根据对应的策略向硬盘进行同步操作。
- 随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
- 当 Redis 重启时,可以加载 AOF 文件进行数据恢复。
命令追加
当 AOF 持久化功能处于打开状态(配置文件中:appendonly yes
)时,Redis 在执行完一个写命令之后,调用feedAppendOnlyFile
函数,以协议格式(也就是RESP,即 Redis 客户端和服务器交互的通信协议 )将被执行的写命令追加到 Redis 服务端维护的 AOF 缓冲区末尾(sds类型变量:aof_buf
)。
比如说 SET mykey myvalue 这条命令就以如下格式记录到 AOF 缓冲中。
1 | "*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n" |
文件写入与同步
Redis的服务器进程是一个事件循环,文件事件负责处理客户端的命令请求,而时间事件负责执行serverCron
函数这样的定时运行的函数。在处理文件事件执行写命令,使得命令被追加到aof_buf
中,然后在处理时间事件执行serverCron
函数会调用flushAppendOnlyFile
函数进行文件的写入和同步。
flushAppendOnlyFile函数的行为由服务器配置的appendfsync
选项的值决定,该选项有三个可选值,分别是always
、everysec
和 no
:
- always: 每执行一个命令保存一次。 Redis 在每个事件循环都要将 AOF 缓冲区中的所有内容写入到 AOF 文件,并且同步 AOF 文件,所以 always 的效率是 appendfsync 选项三个值当中最差的一个,但从安全性来说,也是最安全的。当发生故障停机时,AOF 持久化也只会丢失一个事件循环中所产生的命令数据。
- everysec: 每一秒钟保存一次。 Redis 在每个事件循环都要将 AOF 缓冲区中的所有内容写入到 AOF 文件中,并且每隔一秒就要在子线程中对 AOF 文件进行一次同步。从效率上看,该模式足够快。当发生故障停机时,只会丢失一秒钟的命令数据。
- no:不保存。 将aof_buf中的所有内容写入到aof文件,但不对aof文件同步,
fsync
由操作系统执行。
Redis的 write
操作会触发延迟写(delayed write)机制,在同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失。
延迟写机制: 传统的UNIX实现在内核中设有缓冲区高速缓存或页面高速缓存,大多数磁盘I/O都通过缓冲进行。 当将数据写入文件时,内核通常先将该数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则 并不将其排入输出队列,而是等待其写满或者当内核需要重用该缓冲区以便存放其他磁盘块数据时, 再将该缓冲排入到输出队列,然后待其到达队首时,才进行实际的I/O操作。这种输出方式就被称为延迟写。
而 fsync
针对单个文件操作,对其进行强制硬盘同步,fsync
将阻塞直到写入磁盘完成后返回,保证了数据持久化。
三种AOF模式在安全性和性能方面的区别如下:
- no:
write
和fsync
都由主进程执行,两个操作都会阻塞主进程。因为fsync
操作只会在AOF 关闭或 Redis 关闭时执行,或者由操作系统触发。所以当系统故障宕机,那么丢失数据的数量由操作系统的缓存冲洗策略决定。 - always: 该模式的安全性最高,但性能也是最差的,因为服务器必须阻塞直到命令信息被写入并保存到磁盘之后,才能继续处理请求。
- everysec:
write
操作由主进程执行,阻塞主进程。fsync
操作由子线程执行,不直接阻塞主进程,但fsync
操作完成的快慢会影响write
操作的阻塞时长。因为是一秒执行一次,所以它的安全性高于no
模式,系统故障宕机将会丢失一秒钟的命令数据。
appendfsync
的三个值代表着三种不同的调用 fsync
的策略。调用 fsync
周期越频繁,读写效率就越差,但是相应的安全性越高,发生宕机时丢失的数据越少。
4. AOF数据恢复
AOF文件中包含了重建Redis数据所需的所有命令,所以Redis只要读入并重新执行一遍 AOF 文件里边保存的写命令,就可以还原 Redis 关闭之前的状态。
5. AOF重写
因为AOF持久化是通过保存被执行的写命令来记录Redis状态的,所以随着Redis长时间运行,AOF文件中的内容越来越多,文件的体积也会越来越大,如果不加以控制,Redis通过AOF文件还原数据库需要的时间将会变得很久,同时AOF文件很可能会对Redis甚至宿主主机造成影响。
为了解决上诉问题,Redis提供了AOF文件重写(rewrite)功能。通过该功能,Redis 可以创建一个新的 AOF 文件来替代现有的 AOF 文件。新旧两个 AOF 文件所保存的 Redis 状态相同,但是新的 AOF 文件不会包含任何浪费空间的冗余命令,所以新 AOF 文件的体积通常比旧 AOF 文件的体积要小得很多。
例如:
重写前AOF文件命令记录:
RPUSH list “A”,”B”
RPUSH list “C”,”D”
LPOP list
LPOP list
RPUSH list “E”,”F”重写后AOF文件命令记录:
RPUSH list “C”,”D”,”E”,”F”
如上所示,重写前,AOF文件要保存5条命令,重写后只需要保存一条,所以重写后的文件要小很多。
AOF重写实现
AOF文件重写通过 rewriteAppendOnlyFileBackground()
实现,重写不需要对现有的AOF文件进行任何读取、分析或者写入操作,而是读取服务器当前的数据库状态来实现的(rewriteAppendOnlyFileRio()
)。首先从数据库中读取键对应的值,然后用一条命令去记录键值对,代替之前的多条命令,这就是AOF重写功能实现。
在实际过程中,为了避免在执行命令时造成客户端输入缓冲区溢出,AOF 重写在处理列表、哈希表、集合和有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数量,如果数量超REDIS_AOF_REWRITE_ITEMS_PER_CMD
( 一般为64 )常量,则使用多条命令记录该键的值,而不是一条命令。
AOF重写函数会进行大量的写入操作,调用该函数的线程将被长时间阻塞,所以Redis在子进程中执行AOF重写操作。
- 子进程重写期间,主线程可以继续处理客户端命令请求。
- 子进程带有主线程的内存数据拷贝副本,这样就可以避免与主进程竞争db->dict,在不用锁的情况下,也能保证数据的安全性。
AOF重写期间,主进程依然能接收处理命令,会对现有的Redis数据库进行修改,从而导致AOF重写后的数据与现有的数据库数据不一致。因此,Redis设置了AOF重写缓冲区,在创建子进程后,主进程每执行一个写命令都会写到缓冲区中。在子进程完成重写后,主进程会将AOF重写缓冲区的数据写入到重写后的AOF文件中,以此保证数据的一致性。
函数主要功能
1 | void flushAppendOnlyFile(int force); // 将缓冲区的数据刷入到磁盘文件中 |
主要函数实现
写操作命令追加到AOF缓冲区:
1 | void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) { |
AOF缓冲区的数据刷入到AOF文件中:
1 | void flushAppendOnlyFile(int force) { |
AOF重写:
1 | int rewriteAppendOnlyFileBackground(void) { |
从AOF文件中恢复数据:
1 | int loadAppendOnlyFile(char *filename) { |