Redis持久化RDB原理+伪代码实现
source link: https://aoppp.com/redischi-jiu-hua-rdbyuan-li-wei-dai-ma-shi-xian/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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
命令执行时, 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
常量的长度为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
文件会使用不同的方式来保存它们。
本文为作者原创或转载,允许转载,由憧憬在 aoppp.com发布 转载请说明文章出处。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK