目录

1.1 概述

1.2 AOF 持久化的实现

1.2.1  命令追加

1.2.2 AOF 文件的写入与同步

1.3 AOF文件的载入与数据还原

1.4 AOF 重写

1.4.1 AOF 文件重写的实现

1.4.2 AOF 后台重写


Redis 是一个键值对数据库服务器,服务器中通常包含着任意个非空数据库,而每个非空数据库中有可以包含任意个键值对,为了方便起见,我们将服务器中的非空数据库以及它们的键值对统称数据库状态

Redis 是内存数据库,它将自己的数据库状态储存在内存里,如果不想办法将储存在内存中的数据库状态保存到磁盘里,那么一旦服务器进程退出,服务器中的数据库状态也会消失不见。为了解这个问题,Redis 提供了两种持久化方式—— RDB(Redis DataBase) 和 AOF(Append Only File) ,这可以将 Redis 在内存中的数据库状态保存到磁盘里。

RDB快照并不是很可靠。如果你的电脑突然宕机了,或者电源断了,又或者不小心杀掉了进程,那么最新的数据就会丢失。除了 RDB 持久化功能之外,Redis 还提供了 AOF(Append Only File) 持久化功能。

 AOF (Append Only File)  持久化默认是关闭的,通过将 redis.conf 中将 appendonly no,修改为 appendonly yes 来开启AOF 持久化功能,如果服务器开始了 AOF 持久化功能,服务器会优先使用 AOF 文件来还原数据库状态。只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态。如图:

下面详细介绍一下 AOF 持久化,希望能帮助到其他小伙伴~ 关于 RDB 持久化详解 可参考上一篇。

1.1 概述

AOF 持久化功能则提供了一种更为可靠的持久化方式。 每当 Redis 接受到会修改数据集的命令时,就会把命令追加到 AOF 文件里,当你重启 Redis 时,AOF 文件里的命令会被重新执行一次,重建数据。AOF 持久化如图 1-1-1 所示。

举个例子,如果我们对空白的数据库执行以下写命令,那么数据库中将包含三个键值对:

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

RBD 持久化保存数据库状态的方法是将 msg、fruits、numbers 三个键的键值对保存到 RDB 文件中,而 AOF 持久化保存数据库状态的方法则是将服务器执行的SET、SADD、RPUSH三个命令保存到 AOF 文件中。

被写入 AOF 文件中的所有命令都是以 Redis 的命令请求协议格式保存的,因为 Redis 的命令请求协议是纯文本格式,所以我们可以直接打开一个 AOF 文件,观察里面的内容。

例如,对于之前执行的三个写命令来说,服务器将产生包含以下内容的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文件并还原数据库状态时打印的日志:

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

1.2 AOF 持久化的实现

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

1.2.1  命令追加

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

举例:如果客户端向服务器发送以下命令:

redis> SET KEY VALUE
OK

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

redis> SET KEY VALUE

又例如,如果客户端向服务器发送以下命令:  

redis> RPUSH NUMBERS ONE TWO THREE
(integer) 3

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

redis> RPUSH NUMBERS ONE TWO THREE

以上就是AOF持久化的命令追加步骤的实现原理。

1.2.2 AOF 文件的写入与同步

Redis 的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接受客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像 serverCron 函数这样需要定时运行的函数。

因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到 aof_buf 缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用 flushAppendOnlyFile 函数,考虑是否需要将 aof_buf 缓冲区的内容写入和同步到 AOF 文件里,这个过程可以用伪代码表示:

def evenLoop () :

    while True :

        # 处理文件事件,接受命令请求以及发送命令回复
        # 处理请求时可能会有新内容被追加到 aof_buf 缓冲区
        processFileEvents ()

        # 处理时间事件
        processTimeEvents ()
        
        # 考虑是否要将 aof_buf 中的内容写入和保存到 AOF 文件里
        flushAppendOnlyFile ()

flushAppendOnlyFile 函数的行为由服务器配置的 appendfsync 选项的值来决定,各个不同值产生的行为如下表:

表 1-2-1 不同 appendfsync 值产生 不同的持久化行为
appendfsync 选项值flushAppendOnlyFile 函数行为
always将 aof_buf 缓冲区中的所有内容写入并同步到 AOF 文件

everysec

默认

将 aof_buf 缓冲区中的所有内容写入到 AOF 文件如果上次同步 AOF 文件的时间距离现在超过一秒钟,那么再次对 AOF 文件进行同步,并且这个操作是由一个线程专门负责执行的
no将 aof_buf 缓冲区中的所有内容写入到 AOF 文件但并不对 AOF 文件进行同步,何时同步由操作系统来决定

关于文件的写入和同步说明:

为了提高文件的写入效率,在现代的操作系统中,当用户调用 write 函数,将一些数据写入到文件的时候,操作系统通常会将写入数据暂时保存在一个内存缓冲区里面,等到缓冲区的空间被填满、或者超过了指定的时限之后,才真正的将缓冲区中的数据写入到磁盘里。

这种做法虽然提高了效率,但是也为写入数据带来了安全问题,因为如果计算机发生停机,那么保存在内存缓冲区里的写入数据将会丢失。

为此,系统提供了 fsync 和 fdatafync 两个同步函数,它们可以强制让操作系统立即将缓冲区的数据写入到磁盘里面,从而确保写入数据的安全性。

AOF 文件的写入与同步,就好比你在 window 系统打开一个 word 文档,当你写一些内容时就相当于写入,但是你写的内容并没有真正的保存,而是放在一个缓冲区,如果这时关闭的话内容就会丢失。只有当你点击保存时内容才真正的保存(同步)到磁盘,点击保存就好比调用同步函数 fsync 和 fdatafync。

关于 AOF 持久化的效率和安全性:

服务器配置 appendfsync 选项的值直接决定 AOF 持久化功能的效率和安全性。

  1. 当 appendfsync 的值为 always 时,服务器在每个事件循环都要将 aof_buf 缓冲区中的所有内容写入到 AOF 文件,并且同步 AOF 文件,所以 always 的效率是 appendfsync 选项三个值当中最慢的一个,但从安全性来说,always 也是最安全的,因为即使出现故障停机,AOF 持久化也只会丢失一个事件循环中所产生的命令数据。

  2. 当 appendfsync 的值为 everysec 时,服务器在每个事件循环都要将 aof_buf 缓冲区中的所有内容写入到 AOF 文件,并且每隔一秒就要在子线程中对 AOF 文件进行一次同步。从效率上来讲,everysec 模式足够快,并且就算出现故障停机,数据库也只丢失一秒钟的命令数据。

  3. 当 appendfsync 的值为 no 时,服务器在每个事件循环都要将 aof_buf 缓冲区中的所有内容写入到 AOF 文件至于何时对 AOF 文件进行同步,则由操作系统控制。因为处于 no 模式下的 flushAppendOnlyFile 调用无须执行同步操作,所以该模式下的 AOF 文件写入速度总是最快的,不过因为这种模式会在系统缓存中积累一段时间的写入数据,所以该模式的单次同步时长通常是三种模式中时间最长的。从平摊操作的角度来看,no 模式和 everysec 模式的效率类似,当出现故障停机时,使用 no 模式的服务器将丢失上次同步 AOF 文件之后的所有写命令数据。

1.3 AOF文件的载入与数据还原

因为 AOF 文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍 AOF 文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。

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

1)创建一个不带网络连接的伪客户端(fake client):因为 Redis 的命令只能在客户端上下文中执行,而载入 AOF 文件时所使用的命令直接来源于 AOF 文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行 AOF 文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样。

2)从 AOF 文件中分析并读取出一条写命令。

3)使用伪客户端执行被读出的写命令。

4)一直执行步骤2和步骤3,直到 AOF 文件中的所有写命令都被处理完毕为止。

当完成以上步骤之后,AOF 文件所保存的数据库状态就会被完整地还原出来,整个过程如图 1-3-1 所示。 

例如,对于以下 AOF 文件来说:

redis> SELECT 0
redis> SET msg hello
redis> SADD fruits apple banana chaerry
redis> RPUSH number 128 256 512

服务器首先读入并执行 SELECT 0 命令,之后是 SET msg hello 命令,再之后是 SADD fruits apple banana chaerry 命令,最后是 RPUSH numbers 128 256 512 命令,当这些命令都执行完毕之后,服务器的数据库就被还原到之前的状态了。

1.4 AOF 重写

因为 AOF 持久化是通过保存被执行的写命令来记录数据库状态的,所以随着服务器运行时间的流逝,AOF 文件中的内容会越来越多,文件的体积也会越来越大,如果不加以控制的话,体积过大的 AOF 文件很可能对 Redis 服务器、甚至整个宿主计算机造成影响,并且 AOF 文件的体积越大,使用 AOF 文件来进行数据还原所需的时间就越长。

举个例子,如果客户端执行以下命令:

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 文件体积膨胀的问题,Redis 提供了 AOF 文件重写(rewirte)功能。通过该功能,Redis 服务器可以创建一个新的 AOF 文件,新旧两个 AOF 文件所保存的数据库状态相同,但新 AOF 文件不会包含任何浪费空间的冗余命令,所以新 AOF 文件的体积通常会比旧 AOF 文件的体积小很多。

1.4.1 AOF 文件重写的实现

虽然 Redis 将生成新 AOF 文件替换旧 AOF 文件的功能命名为 “AOF 重写”,但实际上,AOF 文件重写并不需要对现有的 AOF 文件进行任何读取、分析或者写入操作,这个功能是通过读取服务器当前的数据库状态来实现的。

例如上面的 list 的例子,服务器为了保存 list 键的状态,必须在 AOF 文件中写入六条命令。

如果服务器想要尽量少的命令记录 list 键的状态,那么最简单高效的办法不是去读取和分析现有 AOF 文件的内容,而是直接从数据库中读取键 list 的值,然后用一条 RPUSH list "C" "D" "E" "F" "G" 命令来代替保存在 AOF 文件中的六条命令,这样就可以保存 list 键所需的命令从六条减少为一条了。

再举例,如果服务器对 animals 键执行了一下命令:

redis> SADD animals "Cat"                  // {"Cat"}
(integer) 1
redis> SADD animals "Dog" "Panda" "Tiger" // {"Cat", "Dog", "Panda", "Tiger"}
(integer) 3
redis> SREM animals "Cat"                 // {"Dog", "Panda", "Tiger"}
(integer) 1
redis> SADD animals "Lion" "Cat"          // {"Dog", "Panda", "Tiger", "Lion", "Cat"}
(integer) 2                                    

那么为了记录 animals 键的状态,AOF 文件必须保存上面列出的四条命令。

如果服务器想减少保存 animals 键所需命令的数量,那么服务器可以通过读取 animals 键的值,然后用一条 SADD animals "Dog" "Panda" "Tiger" "Lion" "Cat" 命令来代替上面的四条命令,这样就将保存 animals 键所需的命令从四条减少为一条了。

除了上面列举的列表键和集合键之外,其他所有类型的键都可以用同样的方法去减少 AOF 文件中的命令数量。首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是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()


def rewrite_string (key) :

    # 使用 GET 命令获取字符串的值
    value = GET (key)
    
    # 使用 SET 命令重写字符串键
    f.write_command(SET, key, value)


def rewrite_list (key) :
    
    # 使用 LRANG 命令获取列表键包含的所有元素
    item1, item2, ..., itemN = LRANG(key, 0, -1)
    
    # 使用 RPUSH 命令重写列表键
    f.write_command(RPUSH, key, item1, item2, ..., itemN)


def rewrite_hash (key) :
    
    # 使用 HGETALL 命令获取哈希键包含的所有键值对
    field1, value1, field2, value2, ..., fieldN, valueN = HGETALL(key)

    # 使用 HMSET 命令重写哈希键
     f.write_command(HMSET, key, field1, value1, field2, value2, ..., fieldN, valueN)


def rewrite_set (key) :
    
    # 使用 SMEMBERS 命令获取集合包含的所有元素
    elem1, elem2, ..., elemN = SMEMBERS(key)

    # 使用 SADD 命令重写集合键
    f.wirte_command(SADD, key, elem1, elem2, ..., elemN)


def rewrite_sort_set (key) :

    # 使用 ZRANG 命令获取有序集合包含的所有元素
    member1, score1, member2, score2, ..., memberN, scoreN = ZRANG(key, 0 , -1, "WITESCORES")

    # 使用 ZADD 命令重写有序集合键
    f.write_command(ZADD, key, score1, member1, score2, member2, ..., scoreN, memberN)


def rewrite_expire_time (key) :
    
    # 获取毫秒精度的键过期时间戳
    timestamp = get_expire_time_in_unixstamp(key)

    # 使用 PEXPIREAT 命令重写键的过期时间
    f.write_command(PEXPIREAT, kye, timestamp) 

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

注意:

在实际中,为了避免在执行命令是造成客户端输入缓冲区溢出,重写程序在处理列表、哈希表、集合、有序集合这四种可能会带有多个元素的键时,会先检查所包含的元素数量,如果元素数量超过了 redis.h/REDIS_AOF_REWRITE_ITEMS_PRE_CMD 常量的值,那么重写程序将使用多条命令来记录键的值,而不单单使用一条命令。

在目前版本中,REDIS_AOF_REWRITE_ITEMS_PRE_CMD 常量的值是64,也就是说,如果一个集合键包含了超过64个元素,那么重写程序会用多条 SADD 命令来记录这个集合,并且每条命令设置的元素数量也为64个。

1.4.2 AOF 后台重写

上面介绍的 AOF 重写程序 aof_rewrite 函数可以很好的创建一个新 AOF 文件的任务,但是因为这个函数会进行大量的写入操作,所以调用这个函数的线程将被长时间阻塞,因为 Redis 服务器使用单个线程来处理命令请求,所以如果由服务器直接调用aof_rewrite 函数的话,那么在重写 AOF 文件期间,服务器将无法处理客户端发来的命令请求。

所以 Redis 决定将 AOF 重写程序放到子进程里执行,这样做可以同时达到两个目的:

1.子进程进行 AOF 重写期间,服务器(父进程)可以继续处理命令请求。

2.子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性。

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

为了解决数据不一致问题,Redis 服务器设置了一个 AOF 重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当 Redis 服务器执行完一个写命令之后,它会同时将这个写命令发送给 AOF 缓冲区和 AOF 重写缓冲区,如图 1-4-2 所示。

这样一来可以保证:

  • AOF 缓冲区的内容会定期被写入和同步到 AOF 文件,对现有 AOF 文件的处理工作会如常进行。

  • 从创建子进程开始,服务器执行的所有写命令都会被记录到 AOF 重写缓冲区里面。

当子进程完成 AOF 重写工作之后,它会向父进程发送一个信号,父进程在接到该信号后,会调用一个信号处理函数,并执行一下工作:

  1. 将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中,这是新的 AOF 文件所保存的数据库状态将和服务器当前的数据库状态一致。

  2. 对新的 AOF 文件进行改名,原子地(atomic)覆盖现有的 AOF 文件,完成新旧两个 AOF 文件的替换。

这个信号处理函数执行完毕之后,父进程就可以继续像往常一样接受命令请求了。

在整个 AOF 后台重写过程中,只有信号处理函数执行时会对服务器进程(父进程)造成阻塞,在其他时候,AOF 后台重写不会阻塞父进程,这将 AOF 重写对服务器性能造成的影响降到了最低。

以上就是 AOF 后台重写,也即是 BGREWRITEAOF命令的实现原理。

参考:

《Redis设计与实现》  黄建宏 著

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐