Redis设计与实现——RDB持久化
@ Shen Jianan · Monday, Jun 13, 2016 · 6 minute read · Update at Jun 13, 2016

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文件。读入的时候根据类型恢复为相对应的对象。

About Me

2018.02至今 杭州嘉云数据 算法引擎

2017.6-2017.12 菜⻦网络-⼈工智能部-算法引擎

2016.09-2018.06 南京大学研究生

2015.07-2015.09 阿里巴巴-ICBU-实习

2012.09-2016.06 南京大学本科