RDB持久化
RDB文件的创建与载入
SAVE命令会阻塞Redis服务器进程,这期间不能处理任何命令。而BGSAVE会派生一个子进程,由子进程创建RDB文件。
void saveCommand(redisClient *c) {
// BGSAVE 已经在执行中,不能再执行 SAVE
// 否则将产生竞争条件
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
return;
}
// 执行
if (rdbSave(server.rdb_filename) == REDIS_OK) {
addReply(c,shared.ok);
} else {
addReply(c,shared.err);
}
}
void bgsaveCommand(redisClient *c) {
// 不能重复执行 BGSAVE
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
// 不能在 BGREWRITEAOF 正在运行时执行
} else if (server.aof_child_pid != -1) {
addReplyError(c,"Can't BGSAVE while AOF log rewriting is in progress");
// 执行 BGSAVE
} else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {
addReplyStatus(c,"Background saving started");
} else {
addReply(c,shared.err);
}
}
创建RDB文件的函数在rdb.c/rdbSave中,rdbSaveBackground是创建一个线程之后在子线程中调用rdbSave。rdbSave先创建一个临时文件,待创建完成之后再重命名覆盖原来的RDB文件。
写临时文件的过程则是不断遍历数据库,对于有键值对的数据库,写入REDIS_RDB_OPCODE_SELECTDB标识(表明切换数据库)之后,遍历追加键值和过期时间到临时文件中,在保存之前都会写入类型标识来表明接下来保存的是什么。最后写入REDIS_RDB_OPCODE_EOF标识,检验校验和,flush缓存,关闭文件。
AOF通常比RDB更新频率更高,所以如果开启了AOF,Redis服务器启动时默认会使用AOF来加载数据。从RDB文件加载数据的函数是rdb.c/rdbLoad。rdbLoad的过程:
def rdbLoad(*filename){
打开rdb文件并初始化写入流
检查版本号
服务器标记开始载入并计时
while(1){
读入RDB数据标识,决定接下来读取的是什么类型的数据
读取过期时间与接下来的键值对
if(type == REDIS_RDB_OPCODE_EOF){
break
}
if(type == REDIS_RDB_OPCODE_SELECTDB){
读取数据库号码
检查号码正确性
更新db指针到指定的数据库
continue
}
读入
}
}
rdb文件结构
RDB文件的结构如下:
REDIS(字符串常量) | db_version | databases(数据库数据) | EOF(常量) | check_sum |
---|
其中REDIS常量是为了方便服务器判断文件是否为RDB文件,db_version是一个4字节的字符串表示的版本号,databases包含所有数据库的数据,EOF是长度为1字节的结束符,check_sum是校验和。
如果只有数据库0和数据库3有数据,那么扩展databases之后的结构如下:
REDIS | db_version | database 0 | database 3 | EOF | check_sum |
---|
每个数据库的详细结构是这样的:
SELECTDB(字符串常量) | db_number | key_value_pairs |
---|
SELECTDB常量的长度为1字节,表明下面的数据是数据库号码。db_number根据号码的大小不同可以是1、2或者5字节长度。读入db_number之后程序就可以切换到指定的数据库了。key_value_pairs保存了所有的键值对数据,如果键值对带有过期时间,那么过期时间也会一并被记录下来。
kay_value_pair部分
不带过期时间的键值对结构如下:
TYPE | key | value |
---|
带有过期时间的键值对结构如下:
EXPIRETIME_MS(字符串常量,1byte) | ms(8byte) | TYPE | key | value |
---|
TYPE记录的是value的类型,长度为1字节,在rdb.h中定义了类型的可取值范围:
/* Dup object types to RDB object types. Only reason is readability (are we
* dealing with RDB types or with in-memory object types?).
*
* 对象类型在 RDB 文件中的类型
*/
#define REDIS_RDB_TYPE_STRING 0
#define REDIS_RDB_TYPE_LIST 1
#define REDIS_RDB_TYPE_SET 2
#define REDIS_RDB_TYPE_ZSET 3
#define REDIS_RDB_TYPE_HASH 4
/* Object types for encoded objects.
*
* 对象的编码方式
*/
#define REDIS_RDB_TYPE_HASH_ZIPMAP 9
#define REDIS_RDB_TYPE_LIST_ZIPLIST 10
#define REDIS_RDB_TYPE_SET_INTSET 11
#define REDIS_RDB_TYPE_ZSET_ZIPLIST 12
#define REDIS_RDB_TYPE_HASH_ZIPLIST 13
在读入数据的时候会根据TYPE值来确定如何读入与解释这些数据,在rdb.c/rdbLoadObject中就包含了如何根据TYPE值读取与解释数据的逻辑:
/* Load a Redis object of the specified type from the specified file.
*
* 从 rdb 文件中载入指定类型的对象。
*
* On success a newly allocated object is returned, otherwise NULL.
*
* 读入成功返回一个新对象,否则返回 NULL 。
*/
robj *rdbLoadObject(int rdbtype, rio *rdb) {
robj *o, *ele, *dec;
size_t len;
unsigned int i;
// 载入字符串对象
if (rdbtype == REDIS_RDB_TYPE_STRING) {
/* Read string value */
if ((o = rdbLoadEncodedStringObject(rdb)) == NULL) return NULL;
o = tryObjectEncoding(o);
// 载入列表对象
} else if (rdbtype == REDIS_RDB_TYPE_LIST) {
/* Read list value
*
* 读入列表的节点数
*/
if ((len = rdbLoadLen(rdb,NULL)) == REDIS_RDB_LENERR) return NULL;
/* Use a real list when there are too many entries
*
* 根据节点数,创建对象的编码
*/
if (len > server.list_max_ziplist_entries) {
o = createListObject();
} else {
o = createZiplistObject();
}
/* Load every single element of the list
*
* 载入所有列表项
*/
while(len--) {
// 载入字符串对象
if ((ele = rdbLoadEncodedStringObject(rdb)) == NULL) return NULL;
/* If we are using a ziplist and the value is too big, convert
* the object to a real list.
*
* 根据字符串对象,
* 检查是否需要将列表从 ZIPLIST 编码转换为 LINKEDLIST 编码
*/
if (o->encoding == REDIS_ENCODING_ZIPLIST &&
sdsEncodedObject(ele) &&
sdslen(ele->ptr) > server.list_max_ziplist_value)
listTypeConvert(o,REDIS_ENCODING_LINKEDLIST);
// ZIPLIST
if (o->encoding == REDIS_ENCODING_ZIPLIST) {
dec = getDecodedObject(ele);
// 将字符串值推入 ZIPLIST 末尾来重建列表
o->ptr = ziplistPush(o->ptr,dec->ptr,sdslen(dec->ptr),REDIS_TAIL);
decrRefCount(dec);
decrRefCount(ele);
} else {
// 将新列表项推入到链表的末尾
ele = tryObjectEncoding(ele);
listAddNodeTail(o->ptr,ele);
}
}
// 载入集合对象
//还有很多不同类型的处理.....
}
return o;
}
value编码
对于不同TYPE的value,它们value的结构、长度也会有所不同。
REDIS_RDB_TYPE_STRING
这样的TYPE保存的是一个字符串对象,字符串对象的编码可以是REDIS_ENCODING_INT或者REDIS_ENCODING_RAW。
REDIS_ENCODING_INT的内容格式,其中的ENCODING可以是REDIS_RDB_ENC_INT8、REDIS_RDB_ENC_INT16或者REDIS_RDB_ENC_INT32。比如:
REDIS_RDB_ENC_INT8 | 123 |
---|
而如果是REDIS_ENCODING_RAW的编码,那么保存的就是一个字符串值。在开启压缩功能的情况下,字符串值在长度小于等于20字节的时候直接原样保存,如果大于20字节则会被压缩后再保存。
没有被压缩的字符串结构:
len | string |
---|
压缩的字符串结构:
REDIS_RDB_ENC_LZF | compressed_len | origin_len | compressed_string |
---|
REDIS_RDB_ENC_LZF表明已经被LZF算法压缩。通过len变量或者compressed_len变量,程序得以判断字符串到哪个位置结束。
REDIS_RDB_TYPE_LIST
这样的TYPE保存的是REDIS_ENCODING_LINKEDLIST编码的对象,RDB文件通过下面的结构保存这种对象:
list_length | item1 | item2 | …. | itemN |
---|
list_length保存的是列表保存了多少项,而不是占用的内存长度——关于内存长度的记录在每个item中。由于LINKED_LIST中的每一项都是字符串,所以详细的结构是这样:
list_length | len | string | len | string |
---|
REDIS_RDB_TYPE_SET
这个TYPE代表集合,value保存的是REDIS_ENCODING_HT编码的集合对象。结构如下:
set_size | elem1 | elem2 | … | elemN |
---|
每个集合元素都是字符串对象,所以它的结构和上面的REDSI_RDB_TYPE_LIST结构很像。
REDIS_RDB_TYPE_HASH
这个TYPE的哈希表类型保存的也是REDIS_ENCODING_HT编码的集合对象。结构如下:
hash_size | key_value_pair 1 | key_value_pair 2 | … | key_value_pair 3 |
---|
键值对的结构:
key | value |
---|
所以一个哈希表的例子如下:
2 | 1 | “a” | 5 | “apple” | 1 | “b” | 6 | “banana” |
---|
REDIS_RDB_TYPE_ZSET
这个TYPE的有序集合保存的是一个REDIS_ENCODING_SKIPLIST编码的有序集合对象,结构如下:
sorted_set_size | elem1 | elem2 | … | elemN |
---|
一个element的结构又如下:
member1 | score1 |
---|
每一个member和score都是一个字符串,下面是一个有序集合的例子,member分别为pi和e,score分别为3.14和2.7:
2 | 2 | “pi” | 4 | “3.14” | 1 | “e” | 3 | “2.7” |
---|
REDIS_RDB_TYPE_SET_INTSET
REDIS_RDB_TYPE_SET_INTSET表明是整数集合对象,RDB文件先将整数集合转换为字符串对象,然后将字符串对象保存到RDB文件中。
REDIS_RDB_TYPE_LIST_ZIPLIST、REDIS_RDB_TYPE_HASH_ZIPLIST、REDIS_RDB_TYPE_ZSET_ZIPLIST
RDB将压缩列表转换成一个字符串对象,然后将字符串对象保存到RDB文件。读入的时候根据类型恢复为相对应的对象。