内存型数据库 Redis,是如何实现持久化的?
一、导读
Redis是内存数据库,它将字节的数据库状态存储在内存里面,所以如果不想办法将存储在内存里的数据库状态保存到磁盘中,那么Redis服务器进程一旦退出,Redis中的数据库状态也会消失不见......
总所周知,Redis实现持久化主要有两种方式——RDB和AOF,本文主要介绍RDB。
RDB持久化既可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个RDB文件中,如图1:
RDB持久化所生成的RDB文件是一个经过压缩的二进制文件 ,通过该文件可以还原生成RDB文件的数据库状态,如图2:
本文主要介绍:
- Redis服务器保存和载入RDB文件的方法,分析save命令和bgsave命令的实现
- Redis服务器自动保存功能的实现原理
- 分析RDB文件的组成部分以及其结构和含义
二、RDB文件的创建与载入
Redis的save和bgsave命令用于生成RDB文件。
1.SAVE命令
save命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。
2.BGSAVE命令
bgsave命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求。
两个命令创建RDB文件的实际工作由rdb.c/rdbSave函数完成,以下是SAVE和BGSAVE的伪码
RDB文件会在Redis服务器启动的时候载入,但要注意是否有AOF文件。载入RDB文件是由rdb.c/rdbLoad函数完成。
3.服务器状态
接下来通过表格来说明save、bgsave和载入RDB文件时的Redis服务器状态。
||服务器状态|
|--|--|
|SAVE|Redis服务器阻塞,客户端发送的所有命令都被拒绝。|
|BGSAVE|Redis服务器可以处理客户端的命令请求。(1)客户端发送的SAVE命令会被拒绝(原因是为了避免服务器进程与子进程同时执行两个rdbSave调用,防止产生竞争条件)(2)客户端发送的BGSAVE命令会被拒绝(原因是同时执行两个BGSAVE命令也会产生竞争条件)(3)客户端发送的BGREWRITEAOF与BGSAVE不能同时执行,原因是并发出两个子进程,同时执行大量的磁盘写入操作,服务器性能上是会受到一定影响的。|
|载入RDB文件时|服务器处于阻塞状态,直至载入工作完成为止|
三、自动间隔性保存
由于bgsave命令可以在不阻塞服务器的情况下生成RDB文件,所以用户可以通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次bgsave命令。
例如:
只要满足以下三个条件的任意一个,BGSAVE命令就会被执行:
- 服务器在900秒之内,对数据库进行了至少1次修改
- 服务器在300秒之内,对数据库进行了至少10次修改
- 服务器在60 秒之内,对数据库进行了至少10000次修改
1.设置保存条件
当配置文件设置了以下值后:
服务器状态中的保存条件的数据结构如下:
从这个数据结构可以看出,saveparams是Redis服务器的一个数组,数组中的每个元素都是一个saveparam对象,saveparam对象又有两个属性分别为seconds(秒数)和changes(修改数)
2.dirty计数器和lastsave属性
- dirty计数器记录距离上一次成功执行save或bgsave命令之后,服务器对数据库状态进行了多少次修改。
- lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行save或bgsave的时间。
3.检查保存条件是否满足
Redis的服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,其负责检查save选项所设置的保存条件是否已经满足,满足则执行bgsave命令。
四、RDB文件结构
这里主要讲下databases、EOF和check_sum的含义:
databases 部分包含零个或任意多个数据库,以及各个数据库中的键值对数据:
- 若服务器中所有数据库为空,则databases 也为空,长度为0字节
- 若服务器中至少有一个数据库非空,则databases 部分的长度会与数据库所保存的键值对的数量、类型和内容有关。
EOF 常量的长度为1字节,这个常量标志着RDB文件正文内容的结束,当读入程序遇到EOF时,则会感知到所有数据库的所有键值对都已经载入完毕了。
check_sum 是一个8字节的无符号整数,保存着一个校验和,其根据REDIS、dbversion、databases、EOF四个部分的内容进行计算得出的。服务器在载入RDB文件时,会将载入数据所计算出的校验和与checksum所记录的校验和进行对比,以此来检查RDB文件是否出错或损坏。
1.databases部分
一个RDB文件的databases部分可以保存任意多个非空数据库。
此RDB文件保存了0号和6号数据库中的所有键值对数据
|REDIS| dbversion |database 0|database 6|EOF|checksum|
|--|--|--|--|--|--|
而每个非空数据库在RDB文件中都保存以下三个部分
| SELECTDB | dbnumber | keyvalue_pairs|
|--|--|--|
- SELECTDB 常量的长度为1字节,当程序读到这个值时,它知道接下来要读入的将是一个数据库号码。
- dbnumber 保存着一个数据库号码,长度为1、2或5字节,当程序读入dbnumber部分后,服务器会调用select命令,根据读入的数据库号码进行数据库切换,使得之后读入的键值对可以载入到正确的数据库中。
- keyvaluepairs部分保存数据库的所有键值对。若键值对带有过期时间,则过期时间也会和键值对保存在一起。
例如:0号数据库中的结构:
| SELECTDB | 0 | keyvaluepairs|
|--|--|--|
对应于本节开始给出的RDB文件,补充完整如下图:
|REDIS| dbversion | SELECTDB | 0 | pairs0 |SELECTDB |6|paris6|EOF|checksum|
|--|--|--|--|--|--|--|--|--|--|
2.keyvaluepairs部分
keyvaluepairs由以下三部分组成:
| TYPE | key |value|
|--|--|--|
TYPE记录了value的类型,长度1字节,值可以是以下其中一个:
- REDISRDBTYPE_STRING
- REDISRDBTYPE_LIST
- REDISRDBTYPE_SET
- REDISRDBTYPE_ZSET
- REDISRDBTYPE_HASH
- REDISRDBTYPE_LIST_ZIPLIST
- REDISRDBTYPE_SET_INTSET
- REDISRDBTYPE_ZSET_ZIPLIST
- REDISRDBTYPE_HASH_ZIPLIST
key是一个字符串对象,其编码方式和REDISRDBTYPE_STRING类型的value一样。
根据TYPE类型的不同以及保存内容长度的不同,保存value的结构和长度也不同。
带有过期时间的keyvaluepairs结构如下:
|EXPIRETIME_MS|ms| TYPE | key |value|
|--|--|--|--|--|
EXPIRETIME_MS 常量的长度为1字节,用于告知读入程序,接下来要读入一个以毫秒为单位的过期时间。
ms是一个8字节长的带符号整数,记录一个以毫秒为单位的UNIX时间戳即为k-v对的过期时间。
3.value编码
RDB文件中的每个value部分都保存了一个值对象,每个值对象的类型都由与之对应的TYPE记录,根据类型的不同,value部分的结构、长度也会不同。
接下来是各种不同类型的值对象在RDB文件中的保存结构。
(1)字符串对象
如果TYPE值为REDISRDBTYPE_STRING ,那么value保存的值就是一个字符串对象,字符串对象的编码可以是REDIS_ENCODINGINT或REDISENCODING_RAW。
若字符串对象编码为REDISENCODINGINT,则对象中保存的是长度不超过32位的整数。
若字符串对象编码为REDISENCODINGRAW,则有两种方法保存此字符串:
- 若字符串长度小于等于20字节,则这个字符串会被直接原样保存
- 若字符串长度大于20字节,则这个字符串会被压缩后保存
对于未被压缩的字符串,保存结构为:
|len| String |
|--|--|
String保存字符串本身,len保存字符串的长度。
对于压缩后的字符串,保存结构为:
|REDISRDBENC_LZF| compressed_len |originlen| compressedstring|
|--|--|--|--|
REDISRDBENC_LZF常量表示字符串被LZF算法压缩。
当服务器读取到REDISRDBENC_LZF时,会根据之后的compressed_len 、originlen、compressedstring对字符串进行解压缩。
其中compressedstring记录被压缩后的字符串,compressedlen 记录压缩后的长度,origin_len记录字符串原始长度。
(2)列表对象
如果TYPE值为REDISRDBTYPE_LIST,那么value保存的值就是一个REDIS_ENCODING_LINKEDLIST编码的列表对象,如:
|list_length| item1 |item2|...|itemN|
|--|--|--|--|--|
例如这是一个包含三个元素的列表:
|3| 5 |“hello”|5|“world”|1|“!”|
|--|--|--|--|--|--|--|
(3)集合对象
如果TYPE值为REDISRDBTYPE_SET,那么value保存的值就是一个REDIS_ENCODING_HT编码的集合对象,如:
|set_size| item1 |item2|...|itemN|
|--|--|--|--|--|
例如这是一个包含四个元素的集合:
|4| 5 |“apple”|6|“banana”|3|“cat”|3|“dog”|
|--|--|--|--|--|--|--|--|--|
(4)哈希表对象
如果TYPE值为REDISRDBTYPE_HASH,那么value保存的值就是一个REDIS_ENCODING_HT编码的集合对象,如:
|hash_size| k-v 1|k-v 2|...|k-v N|
|--|--|--|--|--|
例如,这是一个包含两个键值对的哈希表
|2| 1|“a”|5|"apple"| 1 |"b"|6|"banana"|
|--|--|--|--|--|--|--|--|--|
(5)有序集合对象
如果TYPE值为REDISRDBTYPE_ZSET,那么value保存的值就是一个REDIS_ENCODING_SKIPLIST编码的集合对象,如:
|sortedsetsize| elem1|elem2|...|elemN|
|--|--|--|--|--|
例如,这是一个带有两个元素的有序集合
|2| 2|“pi”|4|"3.14"| 1 |"e"|3|"2.7"|
|--|--|--|--|--|--|--|--|--|
说明:
- 第一个元素的成员是长度为2的字符串“pi”,分值被转换成字符串之后变成了长度为4的字符串“3.14”
- 第二个元素的成员是长度为1的字符串“e”,分值被转换成字符串之后变成了长度为3的字符串“2.7”
(6)INTSET编码的集合
如果TYPE值为REDISRDBTYPE_SET_INTSET,那么value保存的值就是一个整数集合对象,保存方法是:先将整数集合转换为字符串,然后将这个字符串保存到RDB文件里。
(7)ZIPLIST编码的列表、哈希表或有序集合
如果TYPE值为REDISRDBTYPE_LIST_ZIPLIST、REDISRDBTYPE_HASH_ZIPLIST、REDISRDBTYPE_ZSET_ZIPLIST,那么value保存的就是一个压缩列表对象,RDB文件保存此对象的方法是:将压缩列表转成一个字符串对象,然后将这个字符串保存到RDB文件里。
五、参考文献与总结
参考文献:黄健宏——《Redis设计与实现》
Redis的RDB持久化功能无论是在工程应用上还是在后端开发工程师的面试上都是很重要的知识点,本文是我根据《Redis设计与实现》关于RDB的介绍时的学习总结,希望大家多多支持,如果觉得写得还OK的话,麻烦素质三连!!!
https://mp.weixin.qq.com/s/vr3KsaYWdE_O6Q2fZC8UWw
可以关注我的微信公众号,一起多多交流技术~
版权声明: 本文为 InfoQ 作者【Zhongger】的原创文章。
原文链接:【http://xie.infoq.cn/article/ba6685ee37307e56184d84ddf】。文章转载请联系作者。
评论