简介
- RDB数据落地的2种方式。
- 3种触发RDB数据落地的方式。
- RDB的主要流程。
- object落地的序列化。
- RDB数据落地的互斥。
2种落地方式方式
- 自身进程阻塞式的进行数据落地,这种方式会使得进程阻塞,无法处理其它命令。
- fork子进程进行数据落地,父进程继续处理client的命令,但这个时候父进程会排斥其它落地的请求,任何RDB数据落地在子进程完成之前都会拒绝,同时也会拒绝部分AOF数据落地请求。
3种触发RDB数据落地的方式
关于数据恢复:数据恢复的逻辑比较简单,在redis启动时,判断指定目录下是否有rdb文件,有则通过rdb文件进行数据恢复。
- client通过命令触发RDB,SAVE和BGSAVE命令,分别对应上面的2种落地方式。
- 通过配置文件配置落地条件,在serverCron中定期检查条件是否满足,满足则进行数据落地,该种落地方式默认是fork子进程异步的数据落地。配置的格式如下:
save time_ivl dirty_num // 格式
save 900 1 // 示例1
save 300 10 // 示例2
save 60 1000 // 示例3
配置的含义是,如果距离上次RDB数据落地的时间超过time_ivl且修改的数据的数量大于dirty_num,那么就需要再次RDB数据落地。如果有多个配置,则逐一比较,任何一个条件满足,则进行RDB数据落地。
- 关机前进行数据落地,这种方式通常都是自身进程来执行。
RDB的主要流程
-
RDB数据落地调用的函数为:rdbSaveRio。redisdb的结构如下图,每个redisserver可以有多个db,每个db会有一个dict,存储键值对,其中键为sds字符串类型,值为object,具体的类型由object中的type字段指定。(其实还有expire_dict,存储的是具有过期属性的键和对应的到期时间)
redis db的结构 RDB过程主要伪码:
save(redis_version); // redis的版本号
save(save_info_aux_fields); // rdb的一些辅助字段
save(save_modules_aux); // rdb的辅助模块
for db in server.db_arr:
save(RDB_OPCODE_SELECTDB); // 表示后续数据类型为db的下标
save(db_index); // 存储db的下标
save(RDB_OPCODE_RESIZEDB); // 表示后续的数据是db的空间大小
save(db.db_size); // db的键的dict的size
save(db.expire_size); // db的设置了过期时间的键dict的size
for it in db.db_dict:
save(it.key); // sds db的dict的键
save(it.value); // object db的dict的值
for it in db.expire_dict:
save(it.key); // sds expire_dict中的键
save(it.value); // int expire_dict中到期时间
上面的dict中的key-value在内存中通常是非连续的内存,要进行序列化才能写入到文件中。
object的序列化
1. 主要的object
在了解序列化之前,需要先了解redis中主要有哪些object。
2. object的属性。
redis中,object有2个类型字段,一个是type,一个是encoding,其中type表示上层业务使用对应的类型,encoding表示底层的实现。以Hash为例,在业务层表示的是该结构是键值对类型的数据结构,必须实现键值对所应该具有的接口,hashtable和ziplist是其2种不同的底层实现。
3. 连续内存存储的object
用连续内存存储的object类型(特点是数据都存储在连续的内存中,在落地时,直接将对应内存写到文件中即可)有:int,sds,ziplist,intset,zipmap(已不再使用)。这些object最终都可以通过以下方式写入dump文件。
if ((n = rdbSaveLen(rdb,len)) == -1) return -1; // 先存值的长度
nwritten += n;
if (len > 0) {
if (rdbWriteRaw(rdb,s,len) == -1) return -1; // 再将值写入到文件中
nwritten += len;
}
static int rdbWriteRaw(rio *rdb, void *p, size_t len) {
if (rdb && rioWrite(rdb,p,len) == 0)
return -1;
return len;
}
4. 非连续内存存储的数据的序列化
在这之前需要了解复合的数据类型,redis中的最基本的数据类型是sds和数值(int, double等)这2种类型。其它的类型都是由这2种类型组合而成。上面提到的每个db都有个dict,存储了该db中所有的数据,其中key为sds类型,value为object,object可能的类型和对应的客户端命令关系如下。
命令 | object的type | object的encoding | key的类型 | value的类型 |
---|---|---|---|---|
db.dict(特指db中的dict) | hash | dict | sds | object |
set | string | sds | 无key | 无value |
sadd | set | hashtable/ziplist | sds | 无value |
hset | hash | hashtable/ziplist | sds | sds |
lpush | list | linklist/ziplist/quicklist | sds | 无value |
zset | zset | skiplist/quicklist | sds | double |
从上面的复合数据类型的分析可知,db中的object的复合类型最复杂的hash也只是由sds类型的K-V键值对组成。hashtable的序列化代码如下:
} else if (o->type == OBJ_HASH) {
/* Save a hash value */
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
if ((n = rdbSaveRawString(rdb,o->ptr, ziplist_len)) == -1) return -1;
} else if (o->encoding == OBJ_ENCODING_HT) {
rdbSaveLen(rdb,dictSize((dict*)o->ptr))) // 先存储键值对的数量
while((de = dictNext(di)) != NULL) {
rdbSaveRawString(rdb,(unsigned char*)field, sdslen(field))); // 将key以sds类型进行存储
rdbSaveRawString(rdb,(unsigned char*)value, sdslen(value))); // 将value以sds类型进行存储
}
}
复合的数据类型都是基于sds和数值这2种基本类型组合而成,最终都可以变成基本类型的存储。
数据落地之间的冲突关系
1.RDB数据落地期间,其它的RDB请求都会被拒绝执行,至于AOF数据落地,待确定。
总结
RDB数据落地主要掌握以下几点:
- 2种数据落地方式。
- 3种触发RDB的方式。
- RDB落地的主要流程,了解了该流程即可知道RDB文件内容的格式。
- RDB数据落地需要把每个db中的data_dict和expire_dict进行序列化,其中data_dict的value为object类型,需要知道object类型如何进行序列化。