zoukankan      html  css  js  c++  java
  • Redis持久化

    Redis提供了两种持久化方式:RDB和AOF

      下面,我们来看看上述两者的底层实现原理。

    一、RDB持久化

    1.RDB文件的创建与载入

      在Redis中,有两种方式可以生成RDB文件,一个是SAVE,另一个是BGSAVE

      两者的主要区别是:SAVE命令在进行持久化操作的过程中,会阻塞Redis服务进行,也就是说,在以SAVE方式进行持久化操作的过程中,服务器不能再处理其他的命令请求,这个请求过程必须等到持久化操作结束;BGSAVE命令则是单独开启一个子进程来处理持久化操作。

      上述过程用伪代码表现形式如下:

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

      RDB文件的载入是在Redis服务器启动时,自动载入的,所以Redis并没有专门用于载入RDB文件的命令。只要服务器检测到有RDB文件的存在,它就会自动进行载入 操作。

      关于RDB文件载入过程,值得提一下就是,如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库。

      只有在AOF持久化功能处于关闭状态,Redis服务器才会使用RDBRDB文件来还原数据库状态。

    2.执行save,bgsave命令时,服务器的状态

      在执行save命令时,redis服务器会被阻塞,所以当save命令正在被执行时,客户端发送的所有命令请求都会被拒绝。

      在执行bgsave命令时,由于是子进程在处理持久化操作,所以Redis服务器可以继续处理客户的命令请求。但是,在执行bgsave命令期间,如果客户端又发送来了save,bgsave,bgrewrteaof三个命令其中一个,那么服务器的处理方式会有所不同。

      首先,bgsave 命令正在被子进程执行,那么客户端发来的save命令会直接被服务器拒绝,这是为了避免父进程与子进程同时执行两个rdbSave()调用,防止产生竞争条件。

      其次,bgsave 命令正在被子进程执行,那么客户端发来的bgsave命令也会直接被服务器拒绝,同样也是为了防止产生竞争条件。

      最后,bgsave命令和bgrewrteaof命令不能同时进行,如果bgsave命令正在执行,客户端的bgrewrteaof命令会延迟到bgsave命令执行完毕以后才会执行;如果bgrewrteaof命令正在被执行,那么客户端的bgsave命令会直接被服务器拒绝。这是因为,这两个命令都是由子进程来执行的,不能同时执行主要考虑到性能问题,试想两个并发执行的命令,同时进行大量的读写磁盘操作,这会大大降低服务器性能。

    3.间隔性保存

      上述我们讲到,save命令会阻塞服务器进程,而bgsave命令则会另启一个进程来执行持久化操作。

      因为bgsave命令可以在不阻塞服务器进程来进行持久化,所以redis允许用户通过设置服务器配置的save选项,来让redis间接性的自动执行bgsave命令。

      用户可以在redis.conf文件配置save保存规则,只要其中一个条件满足,服务器就会自动执行bgsave命令。

    save 900 1 # 900秒之内,对数据库进行了一次修改就执行bgsave命令
    save 300 10 # 300秒之内,对数据库进行了十次修改就执行bgsave命令
    save 60 10000 # 60秒之内,对数据库进行了一万次修改就执行bgsave命令
    

      接下来,我们来看看服务器是如何根据上述配置的规则,自动执行bgsave命令。

      我们来看看源码redis.h/redisServer,在这个大的结构体中存在如下一个字段:

    struct redisServer{    
        ...
        struct saveparam *saveparams; //记录了保存条件的数组
        ...
    };
    

      服务器会根据save选项所设置的保存条件,将该值设置到服务器redisServer结构的saveparams属性:

      saveparams属性是一个数组,数组每一个元素都是一个saveparam结构,每个结构都保存了一个save选择设置的保存条件:

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

      上述结构体中的两个参数就是我们设置的,如:save 600 1; 那么seconds=600,changes=1。是不是很神奇!!

      如果有多个条件同时存在的话,那么它的结构如下:

      img

      除了saveparms数组之外,服务器还维持着两个参数:dirty和lastsave.

      其中,dirty记录上一次执行save或者bgsave命令,服务器对数据库状态进行了多少次修改。lastsave则记录上一次执行save或者bgsave命令的时间。

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

      说完了上述,接下来就来说说,redis服务器是如何发现该执行保存操作呢?

      在redis服务器启动之后,内部定期执行执行一个时间事件函数serverCron,这个函数默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,其中一项工作就是检查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()
            # ...    
    

      以上就是redis服务器根据save选项所设置的保存条件,自动执行bgsave命令,进行间隔性数据保存的实现原理。

    二,AOF持久化

       RDB持久化是通过保存数据库中的键值对来记录数据库状态,而AOF持久化则是通过保存Redis服务器所执行的写命令来记录数据状态(如:set key "hello world" 以RDB持久化方式,文件内容为key:hello world,以AOF持久化方式,文件内容为set key "hello world")。

       接下来,我们来看看AOF持久化的实现原理以及减小AOF文件体积的AOF文件重写实现原理。

    1.AOF持久化实现

        这里,我们先说说AOF持久化操作,写入文件的操作并不是单单将命令写入,如set key "hello world",而是将命令按照某种格式进行写入,至于为什么要这样做,后面我们再说。写入文件的内容以某个格式,我们称为协议格式。如上面的命令,则写入文件的如下:*2 $3 set $3 key $5 hello $5 world

       AOF持久化分为三个步骤:命令追加,文件写入,文件同步

    命令追加

       当AOF持久化功能处于打开状态,服务器在执行完一个写命令之后,会以协议格式的形式将被执行的命令追加到服务器aof_buf缓冲区,至于为什么要写入,后面介绍。  

    struct redisServer{
        sds aof_buf; // 写入缓冲区
    };
    

    文件写入与同步

       Redis是单线程架构,也就是说redis服务进程处于一个事件循环中,这个事件循环负责接受来自客户端的命令,以及向客户端发送命令,而时间事件则负责想serverCron函数这样需要定时运行的函数。

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

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

      而flushAppendOnlyFile函数行为由服务器配置redis.conf中的appendsync选项的值来决定。

      appendsync=always/everysec(默认)/no

    2.AOF文件的载入与数据还原

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

    ​ 具体还原过程:

      创建一个不带网络连接的伪客户端,因为redis命令只能在客户端上下文中执行,而载入AOF文件所使用的命令直接来源AOF文件而不是网络连接,所以服务器使用了一个伪客户端来执行AOF文件保存的写命令,效果与客户端执行命令一样。

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

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

      重复上述步骤。

    3.AOF重写

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

      如:
        >rpush list 'a' 'b'
        >rpush list 'c'
        >rpush list 'd'
        >rpush list 'e'

      上述光是记录list状态,AOF文件就要保存五条命令。为了解决上述问题,Redis提供了AOF文件重写功能。

      AOF文件重写并不需要对现有的AOF文件进行任何读取操作,而是根据现有的数据库状态,将其再次进行持久化操作,然后替换保存之前的文件。

      例如上述四条命令是文件记录的,将其还原到redis数据,那么保存在redis数据库中的是如下情景list-->['a','b','c','d','e'],现在我们要进行重写,则根据数据构造出命令:rpush list 'a' 'b' 'c' 'd' 'e'。这样我通过1条命令来代替上面的4条命令,从而大大节约了空间。这就是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):
        # 使用LRANGE命令获取列表键包含的所有元素
        item1,item2,...,itemN = LRANGE(key,0,-1)
    
        # 使用RPUSH命令重写列表键
        f.write_command(RPUSH,key,item1,item2,....,itemN)
    
    def rewrite_hash(key):
        field1,value1,field2,value2,...,fieldN,valueN = HGETALL(key)
        f.write_command(HSET,key,field1,value1,field2,value2,...,fieldN,valueN)
    
    def rewrite_set(key):
        elem1,elem2,...,elemN = SMEMBERS(key)
        f.write_command(SADD,key,elem1,elem2,...,elemN)
    
    def rewrite_sorted_set(key):
        member1,score1,member2,score2,...,memberN,scoreN = ZRANGE(key,0,-1,"WITHSCORES")
        f.write_command(member1,score1,member2,score2,...,memberN,scoreN)
    
    def rewrite_expire_time(key):
        timestamp = get_expire_time_in_unixstamp(key)
        f.write_command(pexpireat,key,timestamp)
    
  • 相关阅读:
    hdu2328 Corporate Identity
    hdu1238 Substrings
    hdu4300 Clairewd’s message
    hdu3336 Count the string
    hdu2597 Simpsons’ Hidden Talents
    poj3080 Blue Jeans
    poj2752 Seek the Name, Seek the Fame
    poj2406 Power Strings
    hust1010 The Minimum Length
    hdu1358 Period
  • 原文地址:https://www.cnblogs.com/jiumo/p/10402724.html
Copyright © 2011-2022 走看看