Redis 是内存数据库,数据库里面的内容都在内存中,为了避免数据库里面的内容丢失,可以通过 RDB 持久化功能,将数据库里面的内容保存到磁盘里面,避免数据丢失。
RDB 持久化功能产生的 RDB文件是一个二进制文件,通过该文件可以还原生成 RDB 文件时的 Redis 数据库状态。
RDB 文件的创建
有两个 Redis 命令可以生成 RDB 文件,一个是 SAVE 命令,一个是 BGSAVE 命令。
SAVE 命令:SAVE 命令会阻塞 Redis 服务器进程,直到 RDB 文件创建完成,在服务器进程阻塞期间,服务器不能处理任何命令请求。
BGSAVE 命令:BGSAVE 命令会派生出一个子进程,然后由子进程创建 RDB 文件,服务器进程可以继续处理命令请求。虽然服务器可以继续处理其他命令请求,但是在 BGSAVE 命令执行期间,服务器处理 BGSAVE 、SAVE 、BGREWRITEAOF 三个命令时和平时会不同。
BGSAVE 命令执行期间,客户端发送的 SAVE 命令会被拒绝,服务器拒绝执行 SAVE 命令和BGSAVE 命令同时执行,是为了避免父进程(服务进程)和子进程同时调用两次 rdbSave 函数,防止产生竞争条件。
BGSAVE 命令执行期间,客户端发送的 BGSAVE 命令也会被拒绝,因为同时执行两个 BGSAVE 命令也会产生竞争条件。
BGREWRITEAOF 命令和 BGSAVE 命令不能同时执行:
- 如果BGSAVE 命令正在执行,那么BGREWRITESOF 命令会被延迟到BGSAVE 命令执行完成之后执行。
- 如果BGREWRITEAOF 命令正在执行,那么客户端发送的 BGSAVE 命令会被服务器拒绝。
虽然BGSAVE、BGREWRITEAOF 命令都是通过子进程执行的,但是,并发两个子进程,同时两个子进程都同时进行大量的磁盘写入操作,会对性能造成影响。
创建 RDB 文件的实际工作是由 rdb.c/rdbSave 函数来完成,SAVE 命令和 BGSAVE 命令是通过不同的方式来调用该函数。
SAVE 命令执行流程
def SAVE(): #创建 RDB文件 rdbSave()
BGSAVE 命令执行流程
def BGSAVE(): #创建子进程 pick = fork() if pid = 0: #子进程创建 RDB 文件 rdbSave() #完成后向父进程发送信号 signal_parent() else pid > 0 #父进程继续处理命令请求,并通过轮询等待子进程的信号 handle_request_and_wait_signal() else #处理出错情况 Handle_fork_error()
RDB 文件的载入
RDB 文件的载入工作是在服务器启动时自动进行的,所以 Redis 没有单独的命令去执行,只要 Redis 服务器在启动时检测到 RDB 文件存在,就会自动载入 RDB 文件。服务器在载入 RDB 文件时,会一直处于阻塞状态,直到载入工作完成(其实,RDB 文件载入是在服务器启动时进行的,也不能执行命令)
因为 AOF 文件的更新频率比 RDB 文件高,所以
- 如果Redis 服务器开启了 AOF 持久化功能,那么服务器优先使用 AOF 文件还原数据库
- 只有在 AOF 持久化功能处于关闭时,服务器才会使用 RDB 文件来还原数据库
RDB 文件的载入实际是由 rdb.c/rdbLoad 函数完成的,载入流程如图所示:
rdbLoad 函数与 rdbSave 函数之间的关系,如图所示
自动间隔保存 RDB 文件
因为 BGSAVE 命令可以在不阻塞服务器的情况下执行,所以 Redis 允许用户通过设置服务器配置的 SAVE 选项,让服务器每间隔一段时间自动执行一次 BGSAVE 命令,来实现自动间隔保存 RDB 文件的目的。
设置保存条件
可以通过 SAVE 选项设置多个保存条件,只要有一个条件满足,服务器就会执行 BGSAVE 命令。可以通过指定配置文件或者传入启动参数的方式设置 save 选项,如果没有设置 save 选项,服务器会为 save 选项设置默认值。
SAVE 选项配置的是服务器状态 redisServer 结构的 saveparams 属性,saveparams 是一个数组,数组中的每个元素都是一个 saveparam 结构,结构代码示例如下,除了 saveparams 数组之外,服务器状态还维持一个dirty 计数器,以及一个 lastsave 属性。
struct saveparam {
// 秒数
time_t seconds;
// 修改次数
int changes;
};
// dirty 记录自上一次成功执行 SAVE 或 BGSAVE 命令后,服务器对数据库进行了多少次的修改(写入,删除,更新等操作)
long long dirty;
// lastsave 是一个 Unix 时间戳,记录了上一次成功执行 SAVE 命令或者BGSAVE 命令的时间
time_t lastsave;
检查保存条件是否满足
Redis 服务器的周期性操作函数 serverCorn 默认每一百毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查 SAVE 选项设置的保存条件是否已经满足,如果满足,就执行 BGSAVE 命令。serverCorn 函数检查保存条件是否满足的流程如下所示:
def serverCorn():
# ...
# 遍历所有的保存条件
for saveparam in server.saveparams:
save_interval = unixtime_now() - server.lastsave
# 如果服务器状态的修改次数超过条件设置的次数
# 并且距离上次保存时间超过条件所设置的时间,执行 BGSAVE 命令
if(server.dirty >= saveparam.changes and save_interval > saveparam.seconds)
BGSAVE()
# ...
RDB 文件结构
RDB 文件保存的是二进制数据,而不是 C 字符串。文件结构如图所示:
REDIS
RDB 的文件的最开头是 REDIS 部分,这个部分的长度是 5 字节,保存着 R E D I S 五个字符,通过这五个字符,可以确认导入的是否是 RDB 文件。
db_version
db_version 长度为 4 个字节,类型是一个字符串表示的整数,这个整数记录了 RDB 文件的版本号,不同的 Redis 版本是不同的,所以 Redis的 rdb 文件存在兼容问题。
database
包含零个或者任意多个数据库,以及多个数据库中的键值对数据。
- 如果服务器的数据状态为空(所有数据库都是空的),那么这个部分也是空的,长度为 0 字节。
- 如果服务器的数据状态为非空(有至少一个数据库为非空),那么这个部分也为非空,根据数据库所保存的键值对的数量,类型和内容不同,这个部分的长度也会有所不同。
EOF
EOF 的常量长度为 1 字节,这个常量标志着RDB 文件的结束,当程度读到这个值时,它就知道所有数据库的所有键值对都已经载入完毕了。
check_sum
一个 8 字节长的无符号整数,保存着一个校验和,这个校验和是程序通过对 REDIS、db_version、database、EOF 四个部分的内容进行计算得出的。服务器在载入 RDB文件时,会将载入数据所计算出的校验和check_sum 所记录的校验和进行对比,看文件是否有出错或损坏的情况。