阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

Redis源码剖析之持久化

197次阅读
没有评论

共计 6559 个字符,预计需要花费 17 分钟才能阅读完成。

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。是不是很神奇!!

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

Redis 源码剖析之持久化

除了 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\r\n$3\r\nset\r\n$3\r\nkey\r\n$5\r\nhello\r\n$5\r\nworld

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)

正文完
星哥玩云-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2022-01-22发表,共计6559字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中