vlambda博客
学习文章列表

内存型数据库Redis,是如何实现持久化的?

内存型数据库Redis,是如何实现持久化的?


 一、导读

Redis是内存数据库,它将字节的数据库状态存储在内存里面,所以如果不想办法将存储在内存里的数据库状态保存到磁盘中,那么Redis服务器进程一旦退出,Redis中的数据库状态也会消失不见......


众所周知,Redis实现持久化主要有两种方式——RDB和AOF,本文主要介绍RDB。


RDB持久化既可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个RDB文件中,如图1:

RDB持久化所生成的RDB文件是一个经过压缩的二进制文件 ,通过该文件可以还原生成RDB文件的数据库状态,如图2:

内存型数据库Redis,是如何实现持久化的?

本文主要介绍:

 - Redis服务器保存和载入RDB文件的方法,分析save命令和bgsave命令的实现

 - Redis服务器自动保存功能的实现原理

 - 分析RDB文件的组成部分以及其结构和含义


二、RDB文件的创建与载入

Redis的save和bgsave命令用于生成RDB文件。

1.SAVE命令

save命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。

redis> SAVE //等待直到RDB文件创建完毕OK


2.BGSAVE命令

bgsave命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求。

redis> BGSAVE //派生子进程,并由子进程创建RDB文件Background saving started


两个命令创建RDB文件的实际工作由rdb.c/rdbSave函数完成,以下是SAVE和BGSAVE的伪码

public static void save(){ rdbSave();}

public static void bgSave(){ PID pid = fork();//创建子进程 if(pid == 0){ rdbSave();//子进程负责创建RDB文件 signal_parent();//完成后向父进程发送信号 }else if(pid > 0){ handle_request_and_wait_signal();//父进程继续处理命令请求,并通过轮询等待子进程的信号 }else{ handle_fork_error();//处理出错情况 }}


RDB文件会在Redis服务器启动的时候载入,但要注意是否有AOF文件。载入RDB文件是由rdb.c/rdbLoad函数完成。

内存型数据库Redis,是如何实现持久化的?

3.服务器状态

接下来通过表格来说明save、bgsave和载入RDB文件时的Redis服务器状态。


服务器状态
SAVE Redis服务器阻塞,客户端发送的所有命令都被拒绝。
BGSAVE Redis服务器可以处理客户端的命令请求。(1)客户端发送的SAVE命令会被拒绝(原因是为了避免服务器进程与子进程同时执行两个rdbSave调用,防止产生竞争条件)(2)客户端发送的BGSAVE命令会被拒绝(原因是同时执行两个BGSAVE命令也会产生竞争条件)(3)客户端发送的BGREWRITEAOF与BGSAVE不能同时执行,原因是并发出两个子进程,同时执行大量的磁盘写入操作,服务器性能上是会受到一定影响的。
载入RDB文件时 服务器处于阻塞状态,直至载入工作完成为止


三、自动间隔性保存

由于bgsave命令可以在不阻塞服务器的情况下生成RDB文件,所以用户可以通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次bgsave命令。


例如:

save 900 1save 300 10save 60 10000


只要满足以下三个条件的任意一个,BGSAVE命令就会被执行:

 - 服务器在900秒之内,对数据库进行了至少1次修改

 - 服务器在300秒之内,对数据库进行了至少10次修改

 - 服务器在60 秒之内,对数据库进行了至少10000次修改



1.设置保存条件

当配置文件设置了以下值后:

save 900 1save 300 10save 60 10000


服务器状态中的保存条件的数据结构如下:

内存型数据库Redis,是如何实现持久化的?

从这个数据结构可以看出,saveparams是Redis服务器的一个数组,数组中的每个元素都是一个saveparam对象,saveparam对象又有两个属性分别为seconds(秒数)和changes(修改数)


2.dirty计数器和lastsave属性

 - dirty计数器记录距离上一次成功执行save或bgsave命令之后,服务器对数据库状态进行了多少次修改。

 - lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行save或bgsave的时间。



3.检查保存条件是否满足

Redis的服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,其负责检查save选项所设置的保存条件是否已经满足,满足则执行bgsave命令。


四、RDB文件结构

内存型数据库Redis,是如何实现持久化的?

这里主要讲下databases、EOF和check_sum的含义:


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

 - 若服务器中所有数据库为空,则databases 也为空,长度为0字节

 - 若服务器中至少有一个数据库非空,则databases 部分的长度会与数据库所保存的键值对的数量、类型和内容有关。


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


check_sum 是一个8字节的无符号整数,保存着一个校验和,其根据REDIS、db_version、databases、EOF四个部分的内容进行计算得出的。服务器在载入RDB文件时,会将载入数据所计算出的校验和与check_sum所记录的校验和进行对比,以此来检查RDB文件是否出错或损坏。


1.databases部分

一个RDB文件的databases部分可以保存任意多个非空数据库。


此RDB文件保存了0号和6号数据库中的所有键值对数据

内存型数据库Redis,是如何实现持久化的?


而每个非空数据库在RDB文件中都保存以下三个部分

内存型数据库Redis,是如何实现持久化的?


 - SELECTDB 常量的长度为1字节,当程序读到这个值时,它知道接下来要读入的将是一个数据库号码。

 - db_number 保存着一个数据库号码,长度为1、2或5字节,当程序读入db_number部分后,服务器会调用select命令,根据读入的数据库号码进行数据库切换,使得之后读入的键值对可以载入到正确的数据库中。

 - key_value_pairs部分保存数据库的所有键值对。若键值对带有过期时间,则过期时间也会和键值对保存在一起。


例如:0号数据库中的结构:

内存型数据库Redis,是如何实现持久化的?


对应于本节开始给出的RDB文件,补充完整如下图:

内存型数据库Redis,是如何实现持久化的?



2.key_value_pairs部分

key_value_pairs由以下三部分组成:

内存型数据库Redis,是如何实现持久化的?


TYPE记录了value的类型,长度1字节,值可以是以下其中一个:


 - REDIS_RDB_TYPE_STRING

 -  REDIS_RDB_TYPE_LIST

 -  REDIS_RDB_TYPE_SET

 -  REDIS_RDB_TYPE_ZSET

 -  REDIS_RDB_TYPE_HASH

 -  REDIS_RDB_TYPE_LIST_ZIPLIST

 -  REDIS_RDB_TYPE_SET_INTSET

 -  REDIS_RDB_TYPE_ZSET_ZIPLIST

 -  REDIS_RDB_TYPE_HASH_ZIPLIST


key是一个字符串对象,其编码方式和REDIS_RDB_TYPE_STRING类型的value一样。


根据TYPE类型的不同以及保存内容长度的不同,保存value的结构和长度也不同。


带有过期时间的key_value_pairs结构如下:


内存型数据库Redis,是如何实现持久化的?

EXPIRETIME_MS 常量的长度为1字节,用于告知读入程序,接下来要读入一个以毫秒为单位的过期时间。

ms是一个8字节长的带符号整数,记录一个以毫秒为单位的UNIX时间戳即为k-v对的过期时间。



3.value编码

RDB文件中的每个value部分都保存了一个值对象,每个值对象的类型都由与之对应的TYPE记录,根据类型的不同,value部分的结构、长度也会不同。

接下来是各种不同类型的值对象在RDB文件中的保存结构。


(1)字符串对象


如果TYPE值为REDIS_RDB_TYPE_STRING ,那么value保存的值就是一个字符串对象,字符串对象的编码可以是REDIS_ENCODING_INT或REDIS_ENCODING_RAW。


若字符串对象编码为REDIS_ENCODING_INT,则对象中保存的是长度不超过32位的整数。


若字符串对象编码为REDIS_ENCODING_RAW,则有两种方法保存此字符串:


 - 若字符串长度小于等于20字节,则这个字符串会被直接原样保存

 - 若字符串长度大于20字节,则这个字符串会被压缩后保存


对于未被压缩的字符串,保存结构为:

内存型数据库Redis,是如何实现持久化的?

String保存字符串本身,len保存字符串的长度。

对于压缩后的字符串,保存结构为:

内存型数据库Redis,是如何实现持久化的?


REDIS_RDB_ENC_LZF常量表示字符串被LZF算法压缩。

当服务器读取到REDIS_RDB_ENC_LZF时,会根据之后的compressed_len 、origin_len、compressed_string对字符串进行解压缩。

其中compressed_string记录被压缩后的字符串,compressed_len 记录压缩后的长度,origin_len记录字符串原始长度。



(2)列表对象

如果TYPE值为REDIS_RDB_TYPE_LIST,那么value保存的值就是一个REDIS_ENCODING_LINKEDLIST编码的列表对象,如:

内存型数据库Redis,是如何实现持久化的?


例如,这是一个包含三个元素的列表:

内存型数据库Redis,是如何实现持久化的?

(3)集合对象

如果TYPE值为REDIS_RDB_TYPE_SET,那么value保存的值就是一个REDIS_ENCODING_HT编码的集合对象,如:

内存型数据库Redis,是如何实现持久化的?

例如这是一个包含四个元素的集合:

内存型数据库Redis,是如何实现持久化的?

(4)哈希表对象

如果TYPE值为REDIS_RDB_TYPE_HASH,那么value保存的值就是一个REDIS_ENCODING_HT编码的集合对象,如:

内存型数据库Redis,是如何实现持久化的?

例如,这是一个包含两个键值对的哈希表

内存型数据库Redis,是如何实现持久化的?


(5)有序集合对象

如果TYPE值为REDIS_RDB_TYPE_ZSET,那么value保存的值就是一个REDIS_ENCODING_SKIPLIST编码的集合对象,如:

例如,这是一个带有两个元素的有序集合


说明:


 - 第一个元素的成员是长度为2的字符串“pi”,分值被转换成字符串之后变成了长度为4的字符串“3.14”

 - 第二个元素的成员是长度为1的字符串“e”,分值被转换成字符串之后变成了长度为3的字符串“2.7”


(6)INTSET编码的集合

如果TYPE值为REDIS_RDB_TYPE_SET_INTSET,那么value保存的值就是一个整数集合对象,保存方法是:先将整数集合转换为字符串,然后将这个字符串保存到RDB文件里。


(7)ZIPLIST编码的列表、哈希表或有序集合

如果TYPE值为REDIS_RDB_TYPE_LIST_ZIPLIST、REDIS_RDB_TYPE_HASH_ZIPLIST、REDIS_RDB_TYPE_ZSET_ZIPLIST,那么value保存的就是一个压缩列表对象,RDB文件保存此对象的方法是:将压缩列表转成一个字符串对象,然后将这个字符串保存到RDB文件里。



五、参考文献与总结

参考文献:黄健宏——《Redis设计与实现》

Redis的RDB持久化功能无论是在工程应用上还是在后端开发工程师的面试上都是很重要的知识点,本文是我根据《Redis设计与实现》关于RDB的介绍时的学习总结,希望大家多多支持,如果觉得写得还OK的话,麻烦素质三连!!!下篇文章应该是会肝操作系统的知识,因为我本月21号要考操作系统的期末考试(大三下的课程因为疫情延迟到大四开学考,难顶),复习的同时也顺带把操作系统和Linux的知识过一遍吧!