Redis数据库——AOF持久化

前言

        除了RDB持久化功能外,Redis还提供了AOF持久化功能。与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态。

Redis数据库——AOF持久化_第1张图片

        举个例子:

[wy@iZbp15ck5g7od1op3kl8xnZ ~]$ redis-cli 
127.0.0.1:6379> set msg "hello"
OK
127.0.0.1:6379> sadd fruits "apple" "banana" "cherry"
(integer) 3
127.0.0.1:6379> rpush numbers 128 256 512
(integer) 3
127.0.0.1:6379> 

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

        被写入AOF文件的所有命令都是以Redis命令请求协议格式保存的,是纯文本格式,可以直接查看AOF文件。

redis7前AOF持久化会生成一个appendonly.aof基础文件,保存路径和rdb路径一样。

redis7会创建一个appendonlydir,然后在这个文件夹中由对应的aof文件。

        appendonly.aof.1.base.rdb作为基础文件。
        appendonly.aof.1.incr.of,appendonly.aof.2.incr.of作为增量文件。
        appendonly.aof.manifest作为清单文件。

Redis数据库——AOF持久化_第2张图片

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

        服务器在启动时,可以通过载入和执行AOF文件中保存的命令来还原服务器关闭之前数据库的状态。

Redis数据库——AOF持久化_第3张图片

一.AOF持久化的实现

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

        1.1 命令追加

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

struct redisServer
{
    ...
    sds aof_buf;      /* AOF buffer, written before entering the event loop */
    ...
};

         举个例子:

Redis数据库——AOF持久化_第4张图片

Redis数据库——AOF持久化_第5张图片

        1.2 AOF文件的写入与同步

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

        因为服务器在处理文件事件时可能会执行命令,使得一些内容被追加到aof_buf缓冲区中,所以在服务器每次结束一个时间循环前,都会调用flushAppendOnlyFile函数。

def eventLoop():
    while True:
        
        #处理文件事件,接受命令并且回复命令
        #处理命令请求时可能会由新内容加到aof_buf缓冲区中
        processFileEvents()

        #处理时间事件
        processTimeEvents()

        #是否将aof_buf缓冲区中的数据写入和保存到aof文件中
        flushAppendOnlyFile()

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

Redis数据库——AOF持久化_第6张图片

         如果用户没有主动为appendfsync选项设置值,那么appendfsync选项的默认值为everysec。

文件的写入和同步:

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

        这种做法虽然提高效率,但也为写入数据带来了安全问题,如果计算机发生停机,会导致缓冲区里的数据丢失。

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

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

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

  • 当appendfsync的值为always时,服务器在每一个事件循环都要将aof_buf缓冲区重的所有内容写入到AOF文件,并且同步到AOF文件,所以always的效率时最慢的一个。但是就安全性来说,always也是最安全的。因为即使出现故障停机,AOF持久化也只会丢失一个事件循环中的所有命令数据。
  • 当appendfsync的值为everysec时,服务器在每个事件循环都要将aof_buf缓冲区的所有内容写入到AOF文件中,并且每隔一秒就要在子线程中对AOF文件进行一次同步。从效率上来讲,everysec模式足够快,并且就算出现故障停机,数据库也只丢失一秒中的命令数据。
  • 当appendfsync的值为no时,服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件中,至于何时对AOF文件进行同步,则由操作系统控制。因为处于no模式下flushAppendOnlyFile调用无须执行同步操作,所以该模式下的AOF文件写入是最块的。不过因为这种模式会在系统缓存中累积一段时间的写入数据,所以该模式下单次同步时长是最长的。从平摊操作的角度来看,no模式和everysec模式效率类似。当出现故障停机时,使用no模式的服务器将丢失上次同步AOF文件之后的所有命令数据。

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

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

        步骤如下:

  • 创建一个不带网络连接的伪客户端:因为Redis命令只能在客户端的上下文中执行。而载入AOF文件时所使用的命令直接来源AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果一样。
  • 从AOF文件分析并读取出一条写命令。
  • 使用伪客户端执行被读出的写命令。
  • 一直执行步骤2和3,直到AOF文件中的命令都被处理完毕为止。

Redis数据库——AOF持久化_第7张图片

        1.4 AOF重写

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

        举个例子:

Redis数据库——AOF持久化_第8张图片

         为了记录list键的状态,AOF文件就需要保存六条命令。在实际的应用程度来说,写命令执行次数和频率会高得多,所以造成的问题也会严重得多。

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

        1.4.1 AOF文件重写的实现

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

        对于下面的情况:

        旧的AOF文件为了保存当前键list的状态,需要在AOF文件中写入六条命令。

Redis数据库——AOF持久化_第9张图片

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

        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_expire(): 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命令获取字符串的值
    val = Get(key)
    #使用set命令重写字符串的键
    f.write_command(SET, key, value)

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

def rewrite_hash(key):
    #使用HGETALL命令获取哈希键
    field1, value1, fleid2, 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.write_command(SADD, key, elem1, elem2,...,elemN)

def rewrite_sorted_set(key):
    #使用ZRANGE命令获取有序集合键包含的所有元素
    member1, score1, member2, score2,...,..., memberN, scoreN = ZRANGE(key, 0, -1, "WITHSOCRES")
    #使用ZADD命令重写有序集合键
    f.write_command(ZADD, key, score1, member1, score2, member2, ..., ..., scoreN, memberN)

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

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

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

        1.4.2 AOF后台重写

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

        所以为了避免这种情况,Redis决定将AOF重写程序放到子进程里执行,这样做可以达到两个目的:

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

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

        下图展示了一个AOF重写的例子。当子进程开始进行文件重写时,数据库中只有k1一个键,但是当子进程完成AOF文件重写之后,服务器进程的数据库已经重新设置了k2,k3,k4三个键,因此,重写后的AOF文件和服务器当前的数据库状态并不一致,新的AOF文件只保存了k1一个键的数据,而服务器数据库现在却有k1,k2,k3,k4四个键。

Redis数据库——AOF持久化_第10张图片

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

Redis数据库——AOF持久化_第11张图片

        这也就是说,在子进程执行AOF重写期间,服务器进程需要执行三个工作:

  • 执行客户端发来的命令。
  • 将执行后的写命令追加到AOF缓冲区。
  • 将执行后的写命令追加到AOF重写缓冲区。

        这样一来可以保证:

  • AOF缓冲区的内容会定期被写入和同步到AOF文件,对现有AOF文件的处理工作会如常进行。
  • 从创建子进程开始,服务器执行的所有命令都会被记录到AOF重写缓冲区里面。

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

  1. 将AOF重写缓冲区中的所有内容写入到新的AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致。
  2. 对新的AOF文件进行改名,原子的覆盖现有的AOF文件,完成新旧两个AOF文件的替换。

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

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

        举个例子:

        当子进程开始重写时,服务器进程的数据库只有k1一个键,当子进程完成AOF文件重写之后,服务器进程的数据库已经多出了k2,k3,k4三个新键。

        在子进程向服务器进程发送信号之后,服务器进程会将保存在AOF重写缓冲区里面记录的k2,k3,k4三个键的命令追加到新的AOF文件的末尾,然后用新的AOF文件替换旧AOF文件,完成AOF后台重写工作。

Redis数据库——AOF持久化_第12张图片

        以上就是BGREWRITEAOF命令的实现原理。 

 

 

         

你可能感兴趣的:(Redis,redis)