Redis持久化RDB原理+伪代码实现
Redis
分别提供了 RDB
和 AOF
两种持久化机制, 本章首先介绍 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
文件。
看到以上输出就是在成功载入 RDB
文件打印的,另外值得一提的是,因为 AOF
文件的更新频率通常比 RDB
文件的更新频率高,所以:
- 如果服务器开启了
AOF
持久化功能,那么服务器会优先使用AOF
文件来还原数据库状态。 - 只有在
AOF
持久化功能处于关闭状态时,服务器才会使用RDB
文件来还原数据库状态。
下图是服务器载入文件时的判断流程:
SAVE和BGSAVE命令执行时服务器的不同状态
SAVE
前面提到过,当 SAVE
命令执行时, Redis
服务器会被阻塞,所以当 SAVE
命令正在执行时,客户端发送的所有命令请求都会被拒绝。
只有在服务器执行完 SAVE
命令、重新开始接受命令请求后,客户端发送的命令才会被处理。
BGSAVE
因为 BGSAVE
命令的保存工作是由子进程执行的,所以在子进程创建 RDB
文件的过程中, Redis
服务器仍然可以继续处理客户端的命令请求,但是,在 BGSAVE
命令执行期间,服务器处理 SAVE
、 BGSAVE
、 BGREWRITEAOF
三个命令的方式会和平时有所不同。
首先,在 BGSAVE
命令执行期间,客户端发送的 SAVE
命令会被服务器拒绝,服务器禁止 SAVE
命令和 BGSAVE
命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个 rdbSave函数
调用,防止产生竞争条件。
其次,在 BGSAVE
命令执行期间,客户端发送的 BGSAVE
命令会被服务器拒绝,因为同时执行两个 BGSAVE
命令也会产生竞争条件
最后, BGREWRITEAOF
和 BGSAVE
两个命令不能同时执行:
- 如果
BGSAVE
命令正在执行,那么客户端发送的BGREWRITEAOF
命令会被延迟到BGSAVE
命令执行完毕之后执行。 - 如果
BGREWRITEAOF
命令正在执行,那么客户端发送的BGSAVE
命令会被服务器拒绝。
因为 BGSAVE
和 BGREWRITEAOF
两个命令的实际工作都由子进程执行,所以这两个命令在操作方面并没有什么冲突的地方,不能同时执行它们只是一个性能方面的考虑。并发出两个子进程,并且这两个子进程都同时执行大量的磁盘写人操作,这怎么想都不会是一个好主意。
服务器在载入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; }
大概用图表示是这样
除了 saveparams
数组之外,服务器状态还维持著一个 dirty
计数器,以及一个 lastsave
属性
dirty
计数器记录距离上一次成功执行SAVE
命令或者BGSAVE
命令之后,服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括写入、删除、更新等操作)。lastsave
属性是一个UNIX
时间戳,记录了服务器上一次成功执行SAVE
命令或者BGSAVE
命令的时间。
例如:
SET message "hello" # 程序此时将 dirty计数器增加1 SADD database Redis MongoDB MariaDB # 程序此时将 dirty计数器增加3
上图就展示了服务器状态中包含的 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
文件所包含的各个部分
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字节长的无符号整数,保存着一个校验和,这个校验和是程序通过对 REDIS
、db_version
、databases
、EOF
四个部分的内容进行计算得出的。服务器在载人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
实际上这些字段是 AOF
和 RDB
通用部分的文件头内容:
- 头5字节固定为
REDIS
- 第6~9共四字节为
RDB
版本号 - 接下来为
redis-ver
和它的值,即redis
版本 - 接着
redis-bits
和它的值,即redis
的位数,值为32或64 - 接着为
ctime
和它的值,RDB
文件创建时间 - 接着为
used-mem
使用内存大小 - 最后是
aof-preamble
和它的值,值为0或1,1表示RDB有效。
但 RDB
文件头在 aof-preamble
之前多了如下三项:
repl-stream-db
在server.master
客户端中选择的数据库repl-id
当前实例replication ID
repl-offset
当前实例复制的偏移量
你学会了
RDB
文件用于保存和还原Redis
服务器所有数据库中的所有键值对教据。SAVE
命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器。BGSAVE
令由子进程执行保存操作,所以该命令不会阻塞服务器。- 服务器状态中会保存所有用
save
选项设置的保存条件,当任意一个保存条件被潮足时,服务器会自动执行BGSAVE
命令。 RDB
文件是一个经过压缩的二进制文件,由多个部分组成。- 对于不同类型的键值对,
RDB
文件会使用不同的方式来保存它们。