Redis持久化RDB原理+伪代码实现

Redis持久化RDB原理+伪代码实现

Redis 分别提供了 RDBAOF 两种持久化机制, 本章首先介绍 Redis 服务器保存和载入 RDB 文件的方法,重点说明 SVAE 命令和 BGSAVE 命令的实现方式。之后,本章会继续介绍 Redis 服务器自动保存功能的实现原理。各个组成部分,并说明这些部分的结构和含义。在本章的最后,我们将对实际的 RDB 文件进行分析和解读,将之前学到的关于 RDB 文件的知识投人到实际应用中。其中还会查看有些伪代码方便理解,本文来源 redis设计与实现,关于 redis 持久化知识比较重要,所以直接看的书,避免走弯路,以这篇文章记录一下。

基本介绍

RDB 持久化既可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个 RDB 文件中。所生成的 RDB 文件是一个经过压缩的二进制文件,通过该文件可以还
原生成RDB文件时的数据库状态。

RDB文件的创建与载入

有两个 Redis 命令可以用于生成 RDB 文件,一个是 SAVE ,另一个是 BGSAVE

  • SAVE 命令会阻塞 Redis 服务器进程,直到 RDB 文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求
  • BGSAVE 命令会派生一个子进程,然后由子进程负责创建 RDB 文件,服务器进程(父进程)继续处理命令请求

创建 RDB 文件的实际工作由 rdb.c/rdbsave 函数完成, SAVE 命令和 BGSAVE 命令会以不同的方式调用这个函数,通过以下伪代码可以明显地看出这两个命令之间的区别:

def SAVE():

    rdbSave()


def BGSAVE():

    pid = fork()

    if pid == 0:

        # 子进程保存 RDB
        rdbSave()

    elif pid > 0:

        # 父进程继续处理请求,并等待子进程的完成信号
        handle_request()

    else:

        # pid == -1
        # 处理 fork 错误
        handle_fork_error()

RDB 文件的载入工作是在服务器启动时自动执行的,载入 RDB 文件的实际工作由 rdb.c/rdbLoad 函教完成,所以 Redis 并没有专门用于载人 RDB 文件的命令,只要 Redis 服务器在启动时检测到 RDB 文件存在,它就会自动载入 RDB 文件。

20201118094452

看到以上输出就是在成功载入 RDB 文件打印的,另外值得一提的是,因为 AOF 文件的更新频率通常比 RDB 文件的更新频率高,所以:

  • 如果服务器开启了 AOF 持久化功能,那么服务器会优先使用 AOF 文件来还原数据库状态。
  • 只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态。

下图是服务器载入文件时的判断流程:

20201118095751

SAVE和BGSAVE命令执行时服务器的不同状态

SAVE

前面提到过,当 SAVE 命令执行时, Redis 服务器会被阻塞,所以当 SAVE 命令正在执行时,客户端发送的所有命令请求都会被拒绝。
只有在服务器执行完 SAVE 命令、重新开始接受命令请求后,客户端发送的命令才会被处理。

BGSAVE

因为 BGSAVE 命令的保存工作是由子进程执行的,所以在子进程创建 RDB 文件的过程中, Redis 服务器仍然可以继续处理客户端的命令请求,但是,在 BGSAVE 命令执行期间,服务器处理 SAVEBGSAVEBGREWRITEAOF 三个命令的方式会和平时有所不同。

首先,在 BGSAVE 命令执行期间,客户端发送的 SAVE 命令会被服务器拒绝,服务器禁止 SAVE 命令和 BGSAVE 命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个 rdbSave函数 调用,防止产生竞争条件。
其次,在 BGSAVE 命令执行期间,客户端发送的 BGSAVE 命令会被服务器拒绝,因为同时执行两个 BGSAVE 命令也会产生竞争条件
最后, BGREWRITEAOFBGSAVE 两个命令不能同时执行:

  • 如果 BGSAVE 命令正在执行,那么客户端发送的 BGREWRITEAOF 命令会被延迟到 BGSAVE 命令执行完毕之后执行。
  • 如果 BGREWRITEAOF 命令正在执行,那么客户端发送的 BGSAVE 命令会被服务器拒绝。

因为 BGSAVEBGREWRITEAOF 两个命令的实际工作都由子进程执行,所以这两个命令在操作方面并没有什么冲突的地方,不能同时执行它们只是一个性能方面的考虑。并发出两个子进程,并且这两个子进程都同时执行大量的磁盘写人操作,这怎么想都不会是一个好主意。

服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成

自动间隔性保存

这个就是利用 BGSAVE 命令,设置相关条件执行命令,例如我们 redis 一般有如下配置:

save 900 1
save 300 10
save 60 10000

以上配置的解释

  • 服务器在 900 秒之内,对数据库进行了至少 1 次修改
  • 服务器在 300 秒之内,对数据库进行了至少 10 次修改
  • 服务器在 60 秒之内,对数据库进行了至少 10000 次修改

自动保存伪代码

struct redisServer {

    // 记录保存条件的数组
    struct saveparam *saveparams;

    // 修改计数器
    long long dirty;

    // 上一次执行保存的时间
    time_t lastsave;

    // ....
}

struct saveparam {

    // 秒数
    time_t seconds;

    // 修改数
    int changes;
}

大概用图表示是这样

20201118104435

除了 saveparams 数组之外,服务器状态还维持著一个 dirty 计数器,以及一个 lastsave 属性

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

例如:

SET message "hello" # 程序此时将 dirty计数器增加1

SADD database Redis MongoDB MariaDB # 程序此时将 dirty计数器增加3

20201118105801

上图就展示了服务器状态中包含的 dirty 计数器和 lastsave 属性,说明如下:

  • dirty 计数器的值为 123 ,表示服务器在上次保存之后,对数据库状态进行了 123 次修改
  • lastsave 属性则记录了服务器上次执行保存操作的时间戳

检查保存条件是否满足

Redis 的服务器周期性操作函数 servercron 默认每隔100毫秒就会执行一次,该函条件是否已经满足,如果满足的话,就执行 BGSAVE 命令。
以下伪代码展示了 servercron 函教检查保存条件的过程:

def serverCron():
    # 遍历所有条件
    for saveparam in server.saveparams:

        # 计算距离上次执行保存操作有多少秒
        save_interval = unixtime_now() - server.lastsave

        # 如果数据库状态的修改次数超过条件所设置的次数 并且距离上次保存的时间超过条件所设置的时间 那么执行保存操作
        if server.dirty >= saveparam.changes and save_interval > saveparam.seconds:
            BGSAVE()

通过以上代码可以得知,程序会遍历并检查 saveparams 数组中所有保存条件,只要有任意一个条件被满足,那么服务器就会执行 BGSAVE 命令

RDB文件结构

以下展示了一个完整 RDB 文件所包含的各个部分

20201118151755

REDIS

RDB 文件的最开头是 REDIS 部分,这个部分的长度为5字节,保存著"REDIS"五个字符。通过这五个字符,程序可以在载入文件时,快速检查所载入的文件是否 RDB 文件。

db_version

db_version 一个四字节长的以字符表示的整数,记录了该文件所使用的 RDB 版本号。目前的 RDB 文件版本为 0009 。因为不同版本的 RDB 文件互不兼容,所以在读入程序时,需要根据版本来选择不同的读入方式。

databases

databases 部分包含着零个或任意多个数据库,以及各个数据库中的键值对数据:

  • 如果服务器的数据库状态为空(所有教据库都是空的),那么这个部分也为空,长度为0字节
  • 如果服务器的数据库状态为非空(有至少一个数据库非空),那么这个部分也为非空,根据数据库所保存键值对的数量、类型和内容不同,这个部分的长度也会有所不同。

EOF

EOF 常量的长度为1字节,这个常量标志着 RDB 文件正文内容的结束,当读入程序遇到这个值的时候,它知道所有数据库的所有键值对都已经载入完毕了

CheckSum

check_sum 是一个8字节长的无符号整数,保存着一个校验和,这个校验和是程序通过对 REDISdb_versiondatabasesEOF四个部分的内容进行计算得出的。服务器在载人RDB文件时,会将载人数据所计算出的校验和与 check_sum 所记录的校验和进行对比,以此来检查 RDB 文件是否有出错或者损坏的情况出现。

Version 5 开始,如果在配置文件中开启 rdbchecksum yes ,会在 RDB 文件的结尾处,用 8 个字节, CRC64 计算整个文件内容的检验和。

举个例子

这个是我最新拉的 redis ,数据为空,我们依次进行分析: od -c rdb.rdb

0000000   R   E   D   I   S   0   0   0   9 372  \t   r   e   d   i   s
0000020   -   v   e   r 005   6   .   0   .   9 372  \n   r   e   d   i
0000040   s   -   b   i   t   s 300   @ 372 005   c   t   i   m   e 302
0000060   M 301 264   _ 372  \b   u   s   e   d   -   m   e   m 302   @
0000100 345  \f  \0 372  \f   a   o   f   -   p   r   e   a   m   b   l
0000120   e 300  \0 377   g 311 203 274 200   T 211 376
0000134

实际上这些字段是 AOFRDB 通用部分的文件头内容:

  1. 头5字节固定为REDIS
  2. 第6~9共四字节为 RDB 版本号
  3. 接下来为 redis-ver 和它的值,即 redis 版本
  4. 接着 redis-bits 和它的值,即 redis 的位数,值为32或64
  5. 接着为 ctime 和它的值, RDB 文件创建时间
  6. 接着为 used-mem 使用内存大小
  7. 最后是 aof-preamble 和它的值,值为0或1,1表示RDB有效。

RDB 文件头在 aof-preamble 之前多了如下三项:

  • repl-stream-dbserver.master 客户端中选择的数据库
  • repl-id 当前实例 replication ID
  • repl-offset 当前实例复制的偏移量

你学会了

  • RDB 文件用于保存和还原 Redis 服务器所有数据库中的所有键值对教据。
  • SAVE 命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器。
  • BGSAVE 令由子进程执行保存操作,所以该命令不会阻塞服务器。
  • 服务器状态中会保存所有用 save 选项设置的保存条件,当任意一个保存条件被潮足时,服务器会自动执行 BGSAVE 命令。
  • RDB 文件是一个经过压缩的二进制文件,由多个部分组成。
  • 对于不同类型的键值对, RDB 文件会使用不同的方式来保存它们。

您的支持是对我最大的鼓励!

发表于: 作者:憧憬。
关注互联网以及分享全栈工作经验的原创个人博客和技术博客,热爱编程,极客精神
Github 新浪微博 SegmentFault 掘金专栏