除了RDB持久化功能之外,Redis还提供了AOF(Append Only File)持久化功能。与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的。
AOF持久化.png
被写入AOF文件的所有命令都是以Redis的命令请求协议格式保存的,因为Redis的命令请求协议是纯文本格式,所以我们可以直接打开一个AOF文件,观察里面的内容。


举个例子,如果我们对空白数据库执行以下命令:

redis> SET msg "hello"
OK
redis> SADD fruits "apple" "banana" "cherry"
(integer) 3
redis> RPUSH numbers 128 256 512
(integer) 3

则AOF文件中的内容是:

*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n
*3\r\n$3\r\nSET\r\n$3\r\nmsg\r\n$5\r\nhello\r\n
*5\r\n$4\r\nSADD\r\n$6\r\nfruits\r\n$5\r\napple\r\n$6\r\nbanana\r\n$6\r\ncherry\r\n
*5\r\n$5\r\nRPUSH\r\n$7\r\nnumbers\r\n$3\r\n128\r\n$3\r\n256\r\n$3\r\n512\r\n

在这个AOF文件里面,除了用于指定数据库的SELECT命令是服务器自动添加的之外,其他都是我们之前通过客户端发送的命令。


服务器在启动时,可以通过载入和执行AOF文件中保存的命令来还原服务器关闭之前的数据库状态,以下就是服务器载入AOF文件(from append only file)并还原数据库状态时打印的日志:

[8321] 05 Sep 11:58:50.448 # Server started, Redisversion 2.9.11
[8321] 05 Sep 11:58:50.449 * DB loaded from append only file: 0.000 seconds
[8321] 05 Sep 11:58:50.449 * The server is now ready to accept connections on port 6379

AOF持久化的实现

AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤。

命令追加

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

struct redisServer {
    // ...
    // AOF缓冲区
    sds aof_buf;
    // ...
};

举个例子,如果我们向服务器发送以下命令:

redis> SET KEY VALUE
OK

那么在服务器执行这个SET命令之后,会将以下内容追加到aof_buf缓冲区末尾:

*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n

AOF文件的写入与同步

服务器会先将文本格式命令存入aof_buf缓冲区,会在特定条件下将缓冲区中的内容保存到AOF文件。而Redis服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数(这是下一篇文章的内容)。


服务器在执行文件事件时可能会产生一些内容并写到aof_buf缓冲区。所以在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否需要将缓冲区中的内容写入和保存到AOF文件里面。

def eventLoop():
    while True:
        # 处理文件事件,接收命令请求以及发送命令回复
        # 处理命令请求时可能会有新内容被追加到 aof_buf 缓冲区中
        processFileEvents()
        
        # 处理时间事件
        processTimeEvents()
        
        # 考虑是否要将 aof_buf 中的内容写入和保存到 AOF 文件里面
        flushAppendOnlyFile()

flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定,默认选项是everysec。

  • always是最安全的,但效率也是最慢的。服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件,并且同步AOF文件,即使出现故障停机,AOF持久化也只会丢失一个事件循环中所产生的命令数据。
  • everysec效率是可以的,并且就算出现故障停机,数据库也只丢失一秒钟的命令数据。因为服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件,并且每隔一秒就要在子线程中对AOF文件进行一次同步。
  • no的写入效率是最快的,因为flushAppendOnlyFile调用无须执行同步操作。服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件,至于何时对AOF文件进行同步,则由操作系统控制。系统提供了fsync和fdatasync两个同步函数,它们可以强制让操作系统立即将缓冲区中的数据写入到硬盘里面,从而确保写入数据的安全性。

AOF文件的载入和还原

每次开启数据库时,服务器都会重新读一遍AOF文件里保存的写命令,还原服务器关闭之前的数据库状态。


在这个过程中,服务器会首先创建一个伪客户端(fake client,这是下下一篇文章的内容)。因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以需要一个伪客户端。然后使用这个客户端根据AOF文件进行模拟写入,和普通客户端写入效果是一样的。
AOF文件载入过程.png

AOF重写

想象一种情况,对Redis写入一条数据,并在服务器运行期间对此条进行了多次修改。

redis> RPUSH list "A" "B"            // ["A", "B"]
(integer) 2

redis> RPUSH list "C"                // ["A", "B", "C"]
(integer) 3

redis> RPUSH list "D" "E"            // ["A", "B", "C", "D", "E"]
(integer) 5

redis> LPOP list                     // ["B", "C", "D", "E"]
"A"

redis> LPOP list                     // ["C", "D", "E"]
"B"

redis> RPUSH list "F" "G"            // ["C", "D", "E", "F", "G"]
(integer) 5

那么光是为了记录这个list键的状态,AOF文件就需要保存六条命令。这就造成了AOF文件体积快速膨胀。


为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写(rewrite)功能。通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小得多。

文件重写功能的实现

虽说将新AOF文件替换旧AOF文件的过程叫做文件重写,实际上整个过程并不需要对旧AOF文件进行任何读取、分析或者写入操作。这个功能是通过读取数据库状态进行逆向生成写入命令实现的。


就像上面的list键,虽说执行了六条命令,在数据库中也就是一条数据而已。然后用一条RPUSH list"C""D""E""F""G"命令来代替保存在AOF文件中的六条命令就可以了。


整个流程就是首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令。

def aof_rewrite(new_aof_file_name):
    # 创建新 AOF 文件
    f = create_file(new_aof_file_name)
    # 遍历数据库
    for db in redisServer.db:
        # 忽略空数据库
        if db.is_empty(): continue
        # 写入SELECT命令,指定数据库号码
        f.write_command("SELECT" + db.id)
        # 遍历数据库中的所有键
        for key in db:
            # 忽略已过期的键
            if key.is_expired(): continue
            # 根据键的类型对键进行重写
            if key.type == String:
                rewrite_string(key)
            elif key.type == List:
                rewrite_list(key)
            elif key.type == Hash:
                rewrite_hash(key)
            elif key.type == Set:
                rewrite_set(key)
            elif key.type == SortedSet:
                rewrite_sorted_set(key)
            # 如果键带有过期时间,那么过期时间也要被重写
            if key.have_expire_time():
                rewrite_expire_time(key)
    # 写入完毕,关闭文件
    f.close()
    
// String类型键的重写函数
def rewrite_string(key):
    # 使用GET命令获取字符串键的值
    value = GET(key)
    # 使用SET命令重写字符串键
    f.write_command(SET, key, value)

// 其他类型的键也有类似功能的函数

def rewrite_expire_time(key):
    # 获取毫秒精度的键过期时间戳
    timestamp = get_expire_time_in_unixstamp(key)
    # 使用PEXPIREAT命令重写键的过期时间
    f.write_command(PEXPIREAT, key, timestamp)

因为aof_rewrite函数生成的新AOF文件只包含还原当前数据库状态所必须的命令,所以新AOF文件不会浪费任何硬盘空间。

AOF后台重写

因为Redis服务器使用单个线程来处理命令请求,所以如果由服务器直接调用aof_rewrite函数的话,那么在重写AOF文件期间,服务期将无法处理客户端发来的命令请求。所以Redis决定将AOF重写程序放到子进程里执行:

  • 子进程进行AOF重写期间,服务器进程(父进程)可以继续处理命令请求。
  • 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性。


不过,使用子进程也有一个问题需要解决,因为子进程在进行AOF重写期间,服务器进程还需要继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致。


为了解决这个问题,Redis服务器又设置了一个AOF重写缓冲区,在创建子进程的时候开始使用。
服务器同时将命令发送给AOF文件和AOF重写缓冲区.png
所以AOF文件重写过程就变成了:

  • 当子进程开始重写时,服务器进程(父进程)的数据库中只有k1一个键,当子进程完成AOF文件重写之后,服务器进程的数据库中已经多出了k2、k3、k4三个新键。
  • 在子进程向服务器进程发送信号之后,服务器进程会将保存在AOF重写缓冲区里面记录的k2、k3、k4三个键的命令追加到新AOF文件的末尾,然后用新AOF文件替换旧AOF文件,完成AOF文件后台重写操作。

AOF文件后台重写过程.png
以上就是AOF后台重写,也即是BGREWRITEAOF命令的实现原理。

Last modification:June 10th, 2020 at 08:42 am
如果觉得我的文章对你有用,请随意赞赏