<Redis>RDB持久化
前言:
Redis是一个键值对数据库服务器,因为它是内存数据库,它将数据都保存在内存里面,所以如果不想办法进行持久化,那么一旦服务器进程退出,服务器中的数据库状态也会消失不见。
为了解决这个问题,Redis提供了RDB持久化功能,这个功能可以将Redis在内存中的数据库状态保存到磁盘里面,避免数据意外丢失。
Redis持久化既可以手动执行,也可以根据配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个RDB文件中,RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态。
从内存生成RDB文件到磁盘的主要函数是rdbSave
从磁盘还原数据库状态到内存的主要函数是rdbLoad
工作原理
·Redis调用fork(fork操作会阻塞),产生一个子进程
·子进程把数据写到一个临时的RDB文件
·当子进程写完新的RDB文件后,把旧的RDB文件替换掉。
SAVE&BGSAVE
有两个Redis命令可以用于生成RDB文件,一个是SAVE,一个是BGSAVE。
SAVE命令会阻塞Redis服务器进程,SAVE的过程不能处理任何命令请求,直到RDB文件创建完毕为止。
BGSAVE命令会fork出一个子进程,子进程负责创建RDB文件,父进程仍然可以处理命令请求。
创建RDB文件的实际工作由rdb.c/rdbSave函数完成,SAVE和BGSAVE命令会以不用的方式调用这个函数,下面用一段伪代码展示它们调用的区别:
def SAVE():
#创建RDB文件
rdbSave()
def BGSAVE():
#创建子进程
pid = fork()
if(pid==0):
#子进程负责创建RDB文件
rdbSave()
#完成以后向父进程发送信号
signal_parent()
elif pid > 0:
#父进程继续处理命令请求,并通过轮询等待子进程的信号
handle_request_and_wait_signal()
else:
#处理出错情况
handle_fork_error()
和使用SAVE或者BGSAVE命令创建RDB文件不同,RDB文件的载入工作是在服务器启动的时候自动执行的,所以Redis没有专门用于载入RDB文件的命令,只要Redis服务器在启动时检测到RDB文件的存在,它就会自动载入RDB文件。
另外,因为AOF文件的更新频率比RDB文件的更新频率高,所以:
·如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态。
·只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态。
载入RDB文件的实际工作由rdb.c/rdbLoad函数完成,这个函数和rdbSave函数之间的关系就是逆向的。
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命令会被服务器拒绝。
BGREWRITEAOF和BGSAVE都是由子进程完成的,虽然没有什么冲突的地方,主要是出于性能的考虑,并发两个子进程同时执行大量的磁盘写入操作,不是什么好主意。
RDB文件载入期间的服务器状态
RDB文件载入期间Redis服务器处于阻塞状态,直到载入工作完成。
自动间隔性保存
用户可以通过save选项设置多个保存条件,只要其中任何一个条件被满足,服务器就会执行BGSAVE命令。
举个例子:
如果我们向服务器提供以下配置:
save 900 1
save 300 10
save 60 10000
那么只要服务器满足以下三个条件中的任意一个,BGSAVE命令就会被执行。
·服务器在900秒以内,对数据库进行了至少1次修改。
·服务器在300秒以内,对数据库进行了至少10次修改。
·服务器在60秒以内,对数据库进行了至少10000次修改。
这也是redis服务器的默认保存条件,如果用户进行了自定义的配置,将会采用用户的配置。
自动保存的实现
服务器程序会根据save选项的配置,设置服务器状态redisServer结构的saveParams属性。
struct saveParams{
// 秒数
time_t seconds;
// 修改数
int changes;
}
dirty计数器和lastsave属性
除了saveparams数组之外,服务器状态还维持着一个dirty计数器,以及一个lastsave属性。
·dirty计数器记录距离上一次成功执行SAVE或者BGSAVE命令之后,服务器对数据库状态进行了多少次修改
·lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行SAVE或者BGSAVE命令的时间。
struct redisServer{
// ....
// 修改计数器
long long dirty;
// 上一次保存的时间
time_t lastsave;
}
检查保存条件是否满足
Redis的服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查save选项所设置的保存条件是否已经满足,如果满足的话,就执行BGSAVE命令。
以下伪代码展示了serverCron函数检查保存条件的过程:
def serverCron():
#...
# 遍历所有保存条件
for saveparam in server.saveparams:
save_interval = unixtime_not() - server.lastsave
// 如果修改次数超过条件且时间超过条件,就执行BGSAVE命令
if server.dirty >= saveparam.changes and \
save_interval > saveparam.seconds:
BGSAVE()
#....
重点回顾
·RDB文件用于保存和还原Redis服务器所有数据库中的所有键值对数据。
·SAVE命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器。
·BGSAVE命令由子进程执行保存操作,所以该命令不会阻塞服务器。
·服务器状态中会保存所有用save选项设置的保存条件,满足任意一个条件服务器就会自动执行BGSAVE命令。
·RDB文件是一个经过压缩的二进制文件,由多个部分组成。
·对于不同类型的键值对,RDB文件会使用不同的方式来保存他们。
·linux操作系统在fork子进程的时候使用“写时复制”技术,由于fork的过程父进程有写入操作,就会触发父子进程的复制操作,复制操作的效率跟父进程的内存大小挂钩,而且会阻塞。所以redis内存越大,fork阻塞的时间越长。
RDB的缺点:
如果你希望在Redis意外停止工作(例如电源中断)的情况下丢失的数据最少的话,那么RDB 不适合你。
虽然你可以配置不同的Save 时间点(例如每隔5 分钟并且对数据集有100 个写的操作),但是Redis要完整的保存整个数据集是一个比较繁重的工作。你通常会每隔5 分钟或者更久做一次完整的保存,万一Redis意外宕机,你可能会丢失几分钟的数据。
·RDB需要经常 Fork子进程来保存数据集到硬盘上,当数据集比较大的时,Fork的过程是非常耗时的,可能会导致 Redis在一些毫秒级内不能响应客户端的请求。如果数据集巨大并且CPU 性能不是很好的情况下,这种情况会持续1 秒,AOF也需要 Fork,但是你可以调节重写日志文件的频率来提高数据集的耐久度。
参考资料:
Scipathi Krishnan <Redis RDB 文件格式>
Scipathi Krishnan <Redis RDB 版本历史>
Redis作者博文<Redis persistence demystified>
黄健宏 <Redis设计与实现>