一、Redis的持久化

     我们都知道内存就是暂时存储程序以及数据的地方,存取速率快,那基于内存的Redis当然不会想在停机/故障的时候丢失数据,这个时候就得想办法将暂时数据存到一个“永久”的地方(磁盘文件中、XML数据文件中),则为持久化。上一节我们的Redis事务中有提到持久性,就让我们来探索一下Redis的持久化是怎么实现的。

redis 持久化 问题 redis持久化怎么实现_持久化

二、RDB持久化方式 【快照方式(snapshotting)】

        在上图我们可以看见RDB文件创建的两个命令分别是SAVE和BGSAVE,实际上这两个方法都是调用的rdbSave函数完成的。

#以不同的方式来调用rdbSave()
def SAVE():
    #创建RDB文件
    rdbSave()

def BGSAVE():
    #创建子进程
    pid = fork()
    if pid == 0:
        #子进程负责创建RDB文件
        rdbSave()
        #完成之后向父进程发送信号
        signal_parent()
    elif pid > 0:
        #父进程继续处理命令请求,并通过轮询等待子进程的信号
        handle_request_and_wait_signal()
    else:
        #处理出错情况
        handle_fork_error()

     据我们所知Redis是可以配置文件或者传入启动参数的方式启动的,所以可以配置save参数来设置自动间隔性保存的属性。没有主动设置save选项,那么服务器会为save选项设置默认条件:

#服务器在900秒之内,
#对数据库进行了至少1次修改,即执行BGSAVE命令
save 900 1
#同上
save 300 10
save 60 10000

 服务器程序会根据save选项所设置的保存条件,设置服务器状态redisServer结构saveparams属性,saveparams属性是一个数组,数组中的每个元素都是一个saveparam结构,每个saveparam结构都保存了一个save选项设置的保存条件。

struct redisServer {
    // ...
    //记录了保存条件的数组
    struct saveparam *saveparams;
    // ...
    //修改计数器
    long long dirty;
    //上一次执行保存的时间
    time_t lastsave;
};

struct saveparam {
    //秒数
    time_t seconds;
    //修改数
    int changes;
};

 既然提到了服务器状态redisServer结构,对于其中的dirty计数器lastsave的UNIX时间戳得了解一下。

  • dirty计数器记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括写入、删除、更新等操作)。当服务器成功执行一个数据库修改命令之后,程序就会对dirty计数器进行更新:命令修改了多少次数据库,dirty计数器的值就增加多少。
  • lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行SAVE命令或者BGSAVE命令的时间。

既然提到了设置保存条件,那问题来了,怎么知道的符合条件呢?

serverCron(),该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查save选项所设置的保存条件是否已经满足,如果满足的话,就执行BGSAVE命令。

def serverCron():
    # ...
    #遍历所有保存条件
    for saveparam in server.saveparams:
        #计算距离上次执行保存操作有多少秒
        save_interval = unixtime_now()-server.lastsave
        #如果数据库状态的修改次数超过条件所设置的次数
        #并且距离上次保存的时间超过条件所设置的时间
        #那么执行保存操作
        if server.dirty >= saveparam.changes and \
           save_interval > saveparam.seconds:
            BGSAVE()
    # ...

  OK,那既然已经知道了RDB文件是怎么样创建的,接下来让我们看看RDB文件是怎么样的?

redis 持久化 问题 redis持久化怎么实现_服务器_02

关于key_value_pairs部分 我们在前面探索Redis的数据类型的时候已经一一的描述了,这里如果有需要再补充。

三、AOF持久化方式

      3.1:通过保持Redis的写命令来记录数据库状态的AOF持久化例如通过SET、SADD、RPUSH等写命令保存到AOF文件中。

我们在本文 的第一张图中有提到AOF持久化的三个步骤:命令追加、文件写入、文件同步

#在Redis的服务器中,命令追加是当AOF持久化功能处于打开状态时,
#服务器在执行完一个写命令之后,会以协议格式将被执行的写命令
#追加到服务器状态的aof_buf缓冲区的末尾
struct redisServer {
    // ...
    // AOF缓冲区
    sds aof_buf;
    // ...
};

文件的写入与同步主要是依靠Redis的文件事件来进行

redis 持久化 问题 redis持久化怎么实现_服务器_03

#上图写入和同步缓冲思路
def eventLoop():
    while True:
        #处理文件事件,接收命令请求以及发送命令回复
        #处理命令请求时可能会有新内容被追加到 aof_buf缓冲区中
        processFileEvents()
        #处理时间事件
        processTimeEvents()
        #考虑是否要将 aof_buf中的内容写入和保存到 AOF文件里面
        flushAppendOnlyFile()
flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定

       3.2:而AOF文件的载入和数据还原息息相关,AOF文件保存了所有写操作命令,只要将AOF文件读入系统并重新执行一遍对应的操作,可恢复服务器关闭之前的数据库状态

redis 持久化 问题 redis持久化怎么实现_服务器_04

       AOF持久化方式的确好用,但将服务器所有的写命令存起来会面临 一个问题,就是随着操作的不断增加,AOF文件体积不断的扩大,为了解决AOF文件存储过大的问题,Redis提供了rewrite(AOF文件重写功能)

redis 持久化 问题 redis持久化怎么实现_redis 持久化 问题_05

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):
        #使用LRANGE命令获取列表键包含的所有元素
        item1, item2, ..., itemN = LRANGE(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.write_command(SADD, key, elem1, elem2, ..., elemN)
    def rewrite_sorted_set(key):
        #使用ZRANGE命令获取有序集合键包含的所有元素
        member1, score1, member2, score2, ..., memberN, scoreN = ZRANGE(key, 0, -1, "WITHSCORES")
        #使用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, key, timestamp)

redis 持久化 问题 redis持久化怎么实现_数据库_06

 

四、服务器停机,Redis持久化怎么应对

如果Redis服务器在执行事务的过程中停机,那么根据服务器所使用的持久化模式,可能有以下情况出现:

  1. 如果服务器运行在无持久化的内存模式下,那么重启之后的数据库将是空白的,因此数据总是一致的。
  2. 如果服务器运行在RDB模式下,那么在事务中途停机不会导致不一致性,因为服务器可以根据现有的RDB文件来恢复数据,从而将数据库还原到一个一致的状态。如果找不到可供使用的RDB文件,那么重启之后的数据库将是空白的,而空白数据库总是一致的。
  3. 如果服务器运行在AOF模式下,那么在事务中途停机不会导致不一致性,因为服务器可以根据现有的AOF文件来恢复数据,从而将数据库还原到一个一致的状态。如果找不到可供使用的AOF文件,那么重启之后的数据库将是空白的,而空白数据库总是一致的。

综上所述,无论Redis服务器运行在哪种持久化模式下,事务执行中途发生的停机都不会影响数据库的一致性。

上一篇文章里面有提到过事务的耐久性,耐久性(持久性)是由Redis的持久化模式决定的。事务的耐久性指的是,当一个事务执行完毕时,执行这个事务所得的结果已经被保存到永久性存储介质(比如硬盘)里面了,即使服务器在事务执行完毕之后停机,执行事务所得的结果也不会丢失。
因为Redis的事务不过是简单地用队列包裹起了一组Redis命令,Redis并没有为事务提供任何额外的持久化功能,所以Redis事务的耐久性由Redis所使用的持久化模式决定:

  1. 当服务器在无持久化的内存模式下运作时,事务不具有耐久性:一旦服务器停机,包括事务数据在内的所有服务器数据都将丢失。
  2. 当服务器在RDB持久化模式下运作时,服务器只会在特定的保存条件被满足时,才会执行BGSAVE命令,对数据库进行保存操作,并且异步执行的BGSAVE不能保证事务数据被第一时间保存到硬盘里面,因此RDB持久化模式下的事务也不具有耐久性。
  3. 当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,程序总会在执行命令之后调用同步(sync)函数,将命令数据真正地保存到硬盘里面,因此这种配置下的事务是具有耐久性的。
  4. 当服务器运行在AOF持久化模式下,并且appendfsync选项的值为everysec时,程序会每秒同步一次命令数据到硬盘。因为停机可能会恰好发生在等待同步的那一秒钟之内,这可能会造成事务数据丢失,所以这种配置下的事务不具有耐久性。
  5. 当服务器运行在AOF持久化模式下,并且appendfsync选项的值为no时,程序会交由操作系统来决定何时将命令数据同步到硬盘。因为事务数据可能在等待同步的过程中丢失,所以这种配置下的事务不具有耐久性。

no-appendfsync-on-rewrite配置选项对耐久性的影响
配置选项no-appendfsync-on-rewrite可以配合appendfsync选项为always或者everysec的AOF持久化模式使用。当no-appendfsync-on-rewrite选项处于打开状态时,在执行BGSAVE命令或者BGREWRITEAOF命令期间,服务器会暂时停止对AOF文件进行同步,从而尽可能地减少I/O阻塞。但是这样一来,关于“always模式的AOF持久化可以保证事务的耐久性”这一结论将不再成立,因为在服务器停止对AOF文件进行同步期间,事务结果可能会因为停机而丢失。因此,如果服务器打开了no-appendfsync-on-rewrite选项,那么即使服务器运行在always模式的AOF持久化之下,事务也不具有耐久性。在默认配置下,no-appendfsync-on-rewrite处于关闭状态。