一 序
本章书上内容较大部分介绍了RDB文件的创建与载入、自动间隔性保存、RDB文件结构。所以本文分为两部分上边参照书上梳理,第二部分看bgsave的源码实现。
Redis是内存型数据库,如果服务器进程退出,服务器中的数据状态也会消失不见,所以需要持久化功能。为了解决这个问题,Redis提供了两种持久化的机制:RDB和AOF。本篇主要整理RDB持久化的部分。
RDB持久化功能生成的RDB文件时一个经过压缩处理的二进制文件,通过该文件可以还原生成EDB文件的数据库状态,如下图所示
二 RDB文件的创建与载入
2.1 RDB文件创建
RDB触发机制分为手动触发和自动触发。
手动触发有两个命令可以用于生成RDB文件,一个是SAVE
,另一个是BGSAVE
。
SAVE
命令会在生成RDB文件过程中阻塞服务器进程,直到RDB文件创建完成。
BGSAVE
命令会派生出一个子进程,然后由子进程负责创建RDB文件,父进程(服务器进程)继续处理命令请求。
自动触发的配置:
save 900 1 //服务器在900秒之内,对数据库执行了至少1次修改
save 300 10 //服务器在300秒之内,对数据库执行了至少10修改
save 60 1000 //服务器在60秒之内,对数据库执行了至少1000修改
// 满足以上三个条件中的任意一个,则自动触发 BGSAVE 操作
// 或者使用命令CONFIG SET 命令配置
2.2 BGSAVE命令执行时服务器状态
1、在BGSAVE
命令执行期间,客户端发送的SAVE
命令会被服务器拒绝。服务器禁止SAVE
与BGSAVE
同时执行是为了避免父进程和子进程同时指向两个rdbSave
调用,防止产生竞争条件。
2、同样的道理,在BGSAVE
执行期间,客户端的BGSAVE
命令也会被服务器拒绝。
3、另外,对于AOF持久化命令BGREWRITEAOF
与BGSAVE
也同样是互斥关系,如果BGSAVE
正在执行,则BGREWRITEAOF
命令会被延迟到BGSAVE
执行完毕之后;而BGREWRITEAOF
命令执行时,服务器会拒绝BGSAVE
命令的执行。
4、而事实上,因为BGREWRITEAOF
命令与BGSAVE
两个命令的实际工作都是由子进程执行,所以这两个命令在操作方面并没有冲突的地方,不能同时执行只是性能方面的考虑--并发处两个子进程,并且这两个子进程同时对磁盘进行大量读写。
书上还画图大概列个流程图。就直接看源码吧,在rdb.c
void saveCommand(client *c) {
// BGSAVE 已经在执行中,不能再执行 SAVE
// 否则将产生竞争条件
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
return;
}
// 执行
if (rdbSave(server.rdb_filename) == C_OK) {
addReply(c,shared.ok);
} else {
addReply(c,shared.err);
}
}
/* BGSAVE [SCHEDULE] */
void bgsaveCommand(client *c) {
int schedule = 0; //SCHEDULE控制BGSAVE的执行,避免和AOF重写进程冲突
/* The SCHEDULE option changes the behavior of BGSAVE when an AOF rewrite
* is in progress. Instead of returning an error a BGSAVE gets scheduled. */
if (c->argc > 1) {
// 设置schedule标志
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) {
schedule = 1;
} else {
addReply(c,shared.syntaxerr);
return;
}
}
// 不能重复执行 BGSAVE
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
} else if (server.aof_child_pid != -1) {
// 不能在 BGREWRITEAOF 正在运行时执行.提上日程
if (schedule) {
server.rdb_bgsave_scheduled = 1;
addReplyStatus(c,"Background saving scheduled");
} else {
addReplyError(c,
"An AOF log rewriting in progress: can't BGSAVE right now. "
"Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenver "
"possible.");
}
// 执行BGSAVE
} else if (rdbSaveBackground(server.rdb_filename) == C_OK) {
addReplyStatus(c,"Background saving started");
} else {
addReply(c,shared.err);
}
}
后面重点看下rdbSaveBackground的实现。
2.3 文件载入
而对于RDB文件的载入而言,其载入工作是在服务器启动时自动执行的,只要RDB服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件。
另外,对于Redis的另一种持久化方式AOF而言,如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态。(因为AOF的更新频率比RDB的高)
三 自动间隔性保存
3.1 间隔性配置
上面2.1节说了自动间隔性配置。服务器会根据save选项所设置的保存条件,设置服务器状态redisServer结构的saveparams属性,saveparams属性是一个数组,每个元素都是一个saveparam结构。
3.2 dirty计数器和lastsave属性
除了saveparams数组之外,Redis服务器状态还维持一个dirty计数器以及一个lastsave属性,这两个属性为判断是否到达save条件提供了便利。
- 1、 dirty计数器记录距离上一次成功执行
SAVE
或BGSAVE
命令之后,服务器对数据库状态进行了多少次修改(包括写入,删除,更新等操作)。 - 2、 lastsave属性是一个UNIX时间戳,记录上一次成功执行
SAVE
或BGSAVE
的时间
3.3 检查保存条件是否满足
Redis服务器周期性执行操作函数server.c/serverCron,默认每隔100毫秒就会执行一次,该函数对于正在运行的服务器进行维护,它的其中一项工作就是检查save选项所设置的保存条件是否已经满足,如果满足的话,执行BGSAVE
命令。
程序会遍历并检查saveparams中的保存状态,只要有任意一个条件被满足,就会执行BGSAVE
操作。serverCorn函数,通过dirty与lastsave来判断saveparams中保存条件是否被满足。
四 RDB 文件结构
这一章书上篇幅最长,其实第8章《redis设计与实现》-第8章object 都有。所以不打算跟书上那样细。大概说下就OK。
如上图所示,RDB文件最开头为REDIS部分,通过这五个字符,程序在载入文件时,快速检查所载入的文件是否是RDB文件。
- db_version表示RDB文件的版本号。
- databases部分包含零个或多个数据库,以及各个数据库中键值对数据;
- EOF常量的长度为1字节,这个标志着RDB文件正文内容结束,当读入程序遇到这个值时,证明所有数据库的键值对载入完毕。
- check_num保存着一个校验和,用于检查RDB文件是否出错或损坏。
4.1 databases部分
RDB文件中的databases部分可以保存任意多个非空数据库。
每个非空数据库在RDB文件中都保存为SELECTDB ,db_number, key_value_pairs三部分:
当程序读取到SELECTDB值时,它知道接下来要读入的是一个数据库号码;
db_number表示的是一个数据库号码,当程序读入db_number部分之后,服务器会调用SELECT命令,根据读入的数据库号码进行数据库切换。
key_value_pairs部分保存数据库中所有键值对,以及过期时间等内容。
4.2 key_value_pairs部分
一个不带过期时间的键值对在RDB文件中由TYPE,key,value三部分组成:
TYPE基类了value的类型,其代表一种对象类型或者底层编码,当服务器读入文件中键值对数据时,程序会根据TYPE的值来决定如何读入和解释value数据。value为不同编码格式时键值对表示实例:这里源码用的就是save跟load的过程。
/* Save the object type of object "o". */
int rdbSaveObjectType(rio *rdb, robj *o) {
switch (o->type) {
case OBJ_STRING:
return rdbSaveType(rdb,RDB_TYPE_STRING);
case OBJ_LIST:
if (o->encoding == OBJ_ENCODING_QUICKLIST)
return rdbSaveType(rdb,RDB_TYPE_LIST_QUICKLIST);
else
serverPanic("Unknown list encoding");
case OBJ_SET:
if (o->encoding == OBJ_ENCODING_INTSET)
return rdbSaveType(rdb,RDB_TYPE_SET_INTSET);
else if (o->encoding == OBJ_ENCODING_HT)
return rdbSaveType(rdb,RDB_TYPE_SET);
else
serverPanic("Unknown set encoding");
case OBJ_ZSET:
if (o->encoding == OBJ_ENCODING_ZIPLIST)
return rdbSaveType(rdb,RDB_TYPE_ZSET_ZIPLIST);
else if (o->encoding == OBJ_ENCODING_SKIPLIST)
return rdbSaveType(rdb,RDB_TYPE_ZSET);
else
serverPanic("Unknown sorted set encoding");
case OBJ_HASH:
if (o->encoding == OBJ_ENCODING_ZIPLIST)
return rdbSaveType(rdb,RDB_TYPE_HASH_ZIPLIST);
else if (o->encoding == OBJ_ENCODING_HT)
return rdbSaveType(rdb,RDB_TYPE_HASH);
else
serverPanic("Unknown hash encoding");
default:
serverPanic("Unknown object type");
}
return -1; /* avoid warning */
}
具体的保存,参见本文后面的源码rdbSaveKeyValuePair()部分。
总结:
DB的优点:
RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据快照。非常适用于备份,全景复制等场景。
Redis 加载RDB恢复数据远远快于AOF的方式。
RDB的缺点:
RDB没有办法做到实时持久化或秒级持久化。因为BGSAVE每次运行的又要进行fork()的调用创建子进程,这属于重量级操作,频繁执行成本过高,因为虽然Linux支持读时共享,写时拷贝(copy-on-write)的技术,但是仍然会有大量的父进程的空间内存页表,信号控制表,寄存器资源等等的复制。
RDB文件使用特定的二进制格式保存,Redis版本演进的过程中,有多个RDB版本,这导致版本兼容的问题。
五 源码
在RDB
持久化之前需要设置一些标识,用来标识服务器当前的状态,定义在server.h/struct redisServer
结构体中。部分如下:
struct redisServer {
// 数据库数组,长度为16
redisDb *db;
// 从节点列表和监视器列表
list *slaves, *qiank; /* List of slaves and MONITORs */
/* RDB / AOF loading information ××××××××××××××××××××××××××××××××××××××××××××××××××××××××××*/
// 正在载入状态
int loading; /* We are loading data from disk if true */
// 设置载入的总字节
off_t loading_total_bytes;
// 已载入的字节数
off_t loading_loaded_bytes;
// 载入的开始时间
time_t loading_start_time;
// 在load时,用来设置读或写的最大字节数max_processing_chunk
off_t loading_process_events_interval_bytes;
// 服务器内存使用的
size_t stat_peak_memory; /* Max used memory record */
// 计算fork()的时间
long long stat_fork_time; /* Time needed to perform latest fork() */
// 计算fork的速率,GB/每秒
double stat_fork_rate; /* Fork rate in GB/sec. */
/* RDB persistence ××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××*/
// 脏键,记录数据库被修改的次数
long long dirty; /* Changes to DB from the last save */
// 在BGSAVE之前要备份脏键dirty的值,如果BGSAVE失败会还原
long long dirty_before_bgsave; /* Used to restore dirty on failed BGSAVE */
// 执行BGSAVE的子进程的pid
pid_t rdb_child_pid; /* PID of RDB saving child */
// 保存save参数的数组
struct saveparam *saveparams; /* Save points array for RDB */
// 数组长度
int saveparamslen; /* Number of saving points */
// RDB文件的名字,默认为dump.rdb
char *rdb_filename; /* Name of RDB file */
// 是否采用LZF压缩算法压缩RDB文件,默认yes
int rdb_compression; /* Use compression in RDB? */
// RDB文件是否使用校验和,默认yes
int rdb_checksum; /* Use RDB checksum? */
// 上一次执行SAVE成功的时间
time_t lastsave; /* Unix time of last successful save */
// 最近一个尝试执行BGSAVE的时间
time_t lastbgsave_try; /* Unix time of last attempted bgsave */
// 最近执行BGSAVE的时间
time_t rdb_save_time_last; /* Time used by last RDB save run. */
// BGSAVE开始的时间
time_t rdb_save_time_start; /* Current RDB save start time. */
// 当rdb_bgsave_scheduled为真时,才能开始BGSAVE
int rdb_bgsave_scheduled; /* BGSAVE when possible if true. */
// rdb执行的类型,是写入磁盘,还是写入从节点的socket
int rdb_child_type; /* Type of save by active child. */
// BGSAVE执行完的状态
int lastbgsave_status; /* C_OK or C_ERR */
// 如果不能执行BGSAVE则不能写
int stop_writes_on_bgsave_err; /* Don't allow writes if can't BGSAVE */
// 无磁盘同步,管道的写端
int rdb_pipe_write_result_to_parent; /* RDB pipes used to return the state */
// 无磁盘同步,管道的读端
int rdb_pipe_read_result_from_child; /* of each slave in diskless SYNC. */
/* time cache ××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××*/
// 保存秒单位的Unix时间戳的缓存
time_t unixtime; /* Unix time sampled every cron cycle. */
// 保存毫秒单位的Unix时间戳的缓存
long long mstime; /* Like 'unixtime' but with milliseconds resolution. */
/* Latency monitor ××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××*/
// 延迟的阀值
long long latency_monitor_threshold;
// 延迟与造成延迟的事件关联的字典
dict *latency_events;
};
再看上面执行bgsave调用的rdbSaveBackground:
int rdbSaveBackground(char *filename) {
pid_t childpid;
long long start;
// 如果当前有正在进行AOF和RDB操作,否则返回C_ERR
if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
//备份当前数据库的脏键值
server.dirty_before_bgsave = server.dirty;
// 最近一次尝试执行 BGSAVE 的时间
server.lastbgsave_try = time(NULL);
// fork() 开始前的时间,记录 fork() 返回耗时用
start = ustime();
//创建子进程
if ((childpid = fork()) == 0) {
int retval;
/* Child */
//关闭监听的套接字(关闭网络)
closeListeningSockets(0);
//设置进程名字,方便识别
redisSetProcTitle("redis-rdb-bgsave");
//执行保存操作,把数据库写入到文件
retval = rdbSave(filename);
// 打印 copy-on-write 时使用的内存数
if (retval == C_OK) {
//如果做RDB的同时父进程正在写入的数据,那么子进程就会拷贝一个份父进程的内存,而不是和父进程共享一份内存
size_t private_dirty = zmalloc_get_private_dirty();
// 将子进程分配的内容写日志
if (private_dirty) {
serverLog(LL_NOTICE,
"RDB: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
}
// 子进程退出,发送信号给父进程,发送0表示BGSAVE成功,1表示失败
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
/* Parent 父进程*/
// 计算出fork的执行时间
server.stat_fork_time = ustime()-start;
// 计算fork的速率,GB/每秒
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
//如果fork执行时长,超过设置的阀值,则要将其加入到一个字典中,与传入"fork"关联,以便进行延迟诊断
latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
// 如果fork出错,报告错误
if (childpid == -1) {
server.lastbgsave_status = C_ERR;
serverLog(LL_WARNING,"Can't save in background: fork: %s",
strerror(errno));
return C_ERR;
}
// 打印 BGSAVE的日志
serverLog(LL_NOTICE,"Background saving started by pid %d",childpid);
server.rdb_save_time_start = time(NULL); //设置BGSAVE开始的时间
server.rdb_child_pid = childpid; //设置负责执行BGSAVE操作的子进程id
server.rdb_child_type = RDB_CHILD_TYPE_DISK; //设置BGSAVE的类型,往磁盘中写入
updateDictResizePolicy();// 关闭自动 rehash,因为里面有copy操作
return C_OK;
}
return C_OK; /* unreached */
}
rdbsave的主要流程:就可以看见RDB文件的初始操作,刚开始生成一个临时的RDB文件,只有在执行成功后,才会进行rename操作,然后以写权限打开文件,然后调用了rdbSaveRio()函数将数据库的内容写到临时的RDB文件,之后进行刷新缓冲区和同步操作,就关闭文件进行rename操作和更新服务器状态。
/* Save the DB on disk. Return C_ERR on error, C_OK on success.
* 将数据库保存到磁盘上。
* 保存成功返回 REDIS_OK ,出错/失败返回 REDIS_ERR 。
*/
int rdbSave(char *filename) {
char tmpfile[256];
char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */
FILE *fp;
rio rdb;
int error = 0;
// 创建临时文件
snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
// 以写方式打开该文件
fp = fopen(tmpfile,"w");
// 打开失败,获取文件目录,写入日志
if (!fp) {
char *cwdp = getcwd(cwd,MAXPATHLEN);
// 写日志信息到logfile
serverLog(LL_WARNING,
"Failed opening the RDB file %s (in server root dir %s) "
"for saving: %s",
filename,
cwdp ? cwdp : "unknown",
strerror(errno));
return C_ERR;
}
// 初始化一个rio对象,该对象是一个文件对象IO(关于RIO这块,有待整理)
rioInitWithFile(&rdb,fp);
// 将数据库的内容写到rio中
if (rdbSaveRio(&rdb,&error) == C_ERR) {
errno = error;
goto werr;
}
/* Make sure data will not remain on the OS's output buffers */
// 冲洗缓存,确保数据已写入磁盘
if (fflush(fp) == EOF) goto werr;
// 将fp指向的文件同步到磁盘中
if (fsync(fileno(fp)) == -1) goto werr;
// 关闭文件
if (fclose(fp) == EOF) goto werr;
/* Use RENAME to make sure the DB file is changed atomically only
* if the generate DB file is ok. */
//使用 RENAME ,原子性地对临时文件进行改名,覆盖原来的 RDB 文件。
if (rename(tmpfile,filename) == -1) {
// 改变名字失败,则获得当前目录路径,发送日志信息,删除临时文件
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Error moving temp DB file %s on the final "
"destination %s (in server root dir %s): %s",
tmpfile,
filename,
cwdp ? cwdp : "unknown",
strerror(errno));
unlink(tmpfile);
return C_ERR;
}
// 写入完成,打印日志
serverLog(LL_NOTICE,"DB saved on disk");
server.dirty = 0;// 清零数据库脏状态
server.lastsave = time(NULL); // 更新上一次SAVE操作的时间
server.lastbgsave_status = C_OK;// 更新SAVE操作的状态
return C_OK;
werr:// rdbSaveRio()函数的写错误处理,
serverLog(LL_WARNING,"Write error saving DB on disk: %s", strerror(errno));//写日志
fclose(fp);//关闭文件
unlink(tmpfile);//删除临时文件
return C_ERR;//发送C_ERR
}
rdbSaveRio( )我们已经清楚的看到往RDB
文件中写了什么内容。这里可以结合上面的RDB文件结构去对应。
/* Produces a dump of the database in RDB format sending it to the specified
* Redis I/O channel. On success C_OK is returned, otherwise C_ERR
* is returned and part of the output, or all the output, can be
* missing because of I/O errors.
*将一个RDB格式文件内容写入到rio中,成功返回C_OK,否则C_ERR和一部分或所有的出错信息
* When the function returns C_ERR and if 'error' is not NULL, the
* integer pointed by 'error' is set to the value of errno just after the I/O
* error. */
int rdbSaveRio(rio *rdb, int *error) {
dictIterator *di = NULL;
dictEntry *de;
char magic[10];
int j;
long long now = mstime();
uint64_t cksum;
// 开启了校验和选项
if (server.rdb_checksum)
// 设置校验和的函数
rdb->update_cksum = rioGenericUpdateChecksum;
// 将Redis版本信息保存到magic中
snprintf(magic,sizeof(magic),"REDIS%04d",RDB_VERSION);
// 将magic写到rio中
if (rdbWriteRaw(rdb,magic,9) == -1) goto werr;
// 将rdb文件的默认信息写到rio中
if (rdbSaveInfoAuxFields(rdb) == -1) goto werr;
// 遍历所有服务器内的数据库
for (j = 0; j < server.dbnum; j++) {
redisDb *db = server.db+j; //当前的数据库指针
dict *d = db->dict; //当数据库的键值对字典
if (dictSize(d) == 0) continue; // 跳过为空的数据库
di = dictGetSafeIterator(d); // 创建键空间迭代器
if (!di) return C_ERR;
/* Write the SELECT DB opcode */
// 写入数据库的选择标识码 RDB_OPCODE_SELECTDB为254
if (rdbSaveType(rdb,RDB_OPCODE_SELECTDB) == -1) goto werr;
// 写入数据库的id,占了一个字节的长度
if (rdbSaveLen(rdb,j) == -1) goto werr;
/* Write the RESIZE DB opcode. We trim the size to UINT32_MAX, which
* is currently the largest type we are able to represent in RDB sizes.
* However this does not limit the actual size of the DB to load since
* these sizes are just hints to resize the hash tables. */
// 写入调整数据库的操作码,我们将大小限制在UINT32_MAX以内,这并不代表数据库的实际大小,只是提示去重新调整哈希表的大小
uint32_t db_size, expires_size;
// 如果字典的大小大于UINT32_MAX,则设置db_size为最大的UINT32_MAX
db_size = (dictSize(db->dict) <= UINT32_MAX) ?
dictSize(db->dict) :
UINT32_MAX;
// 设置有过期时间键的大小超过UINT32_MAX,则设置expires_size为最大的UINT32_MAX
expires_size = (dictSize(db->expires) <= UINT32_MAX) ?
dictSize(db->expires) :
UINT32_MAX;
// 写入调整哈希表大小的操作码,RDB_OPCODE_RESIZEDB = 251
if (rdbSaveType(rdb,RDB_OPCODE_RESIZEDB) == -1) goto werr;
// 写入提示调整哈希表大小的两个值,如果
if (rdbSaveLen(rdb,db_size) == -1) goto werr;
if (rdbSaveLen(rdb,expires_size) == -1) goto werr;
/* Iterate this DB writing every entry */
//遍历数据库,并写入每个键值对的数据
while((de = dictNext(di)) != NULL) {
sds keystr = dictGetKey(de); //当前键
robj key, *o = dictGetVal(de);//当前键的值
long long expire;
// 根据 keystr ,在栈中创建一个 key 对象
initStaticStringObject(key,keystr);
// 当前键的过期时间
expire = getExpire(db,&key);
// 保存键值对数据,写入到RIO
if (rdbSaveKeyValuePair(rdb,&key,o,expire,now) == -1) goto werr;
}
dictReleaseIterator(di);//释放迭代器
}
di = NULL; /* So that we don't release it again on error. */
/* EOF opcode 写入 EOF 代码 */
if (rdbSaveType(rdb,RDB_OPCODE_EOF) == -1) goto werr;
/* CRC64 checksum. It will be zero if checksum computation is disabled, the
* loading code skips the check in this case. */
// CRC64 校验和。 如果校验和功能已关闭,那么 rdb.cksum 将为 0 ,
// 在这种情况下, RDB 载入时会跳过校验和检查。
cksum = rdb->cksum;
memrev64ifbe(&cksum);
if (rioWrite(rdb,&cksum,8) == 0) goto werr;
return C_OK;
werr://处理写入错误
if (error) *error = errno;//保存错误码
if (di) dictReleaseIterator(di);//如果没有释放迭代器,则释放
return C_ERR;
}
里面分别调用rdbSaveInfoAuxFields()
函数写入一些默认的辅助信息、rdbSaveKeyValuePair写入键值信息,具体如下源码:
/* Save a few default AUX fields with information about the RDB generated. */
// 将一个rdb文件的默认信息写入到rio中
int rdbSaveInfoAuxFields(rio *rdb) {
// 判断主机的总线宽度,是64位还是32位
int redis_bits = (sizeof(void*) == 8) ? 64 : 32;
/* Add a few fields about the state when the RDB was created. */
// 添加rdb文件的状态信息:Redis版本,redis位数,当前时间和Redis当前使用的内存数
if (rdbSaveAuxFieldStrStr(rdb,"redis-ver",REDIS_VERSION) == -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"redis-bits",redis_bits) == -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"ctime",time(NULL)) == -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"used-mem",zmalloc_used_memory()) == -1) return -1;
return 1;
}
/* Save a key-value pair, with expire time, type, key, value.
* 将键值对的键、值、过期时间和类型写入到 RDB 中。
* On error -1 is returned.
* On success if the key was actually saved 1 is returned, otherwise 0
* is returned (the key was already expired). */
int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,
long long expiretime, long long now)
{
/* Save the expire time */
//保存键的过期时间
if (expiretime != -1) {
/* If this key is already expired skip it */
//不写入已经过期的键
if (expiretime < now) return 0;
if (rdbSaveType(rdb,RDB_OPCODE_EXPIRETIME_MS) == -1) return -1;
if (rdbSaveMillisecondTime(rdb,expiretime) == -1) return -1;
}
/* Save type, key, value */
//保存类型,键,值
if (rdbSaveObjectType(rdb,val) == -1) return -1;
if (rdbSaveStringObject(rdb,key) == -1) return -1;
if (rdbSaveObject(rdb,val) == -1) return -1;
return 1;
}
这里key都是string类型的,value就不同了,看下源码rdbSaveObject:
/* Save a Redis object. Returns -1 on error, number of bytes written on success. */
ssize_t rdbSaveObject(rio *rdb, robj *o) {
ssize_t n = 0, nwritten = 0;
if (o->type == OBJ_STRING) {
/* Save a string value */
// 存放字符串对象值
if ((n = rdbSaveStringObject(rdb,o)) == -1) return -1;
nwritten += n;
} else if (o->type == OBJ_LIST) {
/* Save a list value */
//保存链表对象值
if (o->encoding == OBJ_ENCODING_QUICKLIST) {
quicklist *ql = o->ptr;
quicklistNode *node = ql->head;
// 存放长度
if ((n = rdbSaveLen(rdb,ql->len)) == -1) return -1;
nwritten += n;
do {// 判断节点是否能压缩
if (quicklistNodeIsCompressed(node)) {
void *data;
// 如能,采用Lzf压缩后写入
size_t compress_len = quicklistGetLzf(node, &data);
//会保存原始长度,压缩后的长度,压缩后的字符串
if ((n = rdbSaveLzfBlob(rdb,data,compress_len,node->sz)) == -1) return -1;
nwritten += n;
} else {// 如果不能则直接写入
if ((n = rdbSaveRawString(rdb,node->zl,node->sz)) == -1) return -1;
nwritten += n;
}
} while ((node = node->next));
} else {//编码错误
serverPanic("Unknown list encoding");
}
} else if (o->type == OBJ_SET) {
/* Save a set value */
// 存放集合对象
if (o->encoding == OBJ_ENCODING_HT) {
// 字典编码的时候,存放长度和键值
dict *set = o->ptr;
//创建字典迭代器
dictIterator *di = dictGetIterator(set);
dictEntry *de;
if ((n = rdbSaveLen(rdb,dictSize(set))) == -1) return -1;
nwritten += n;
// 遍历每一个键,并存放在RDB中
while((de = dictNext(di)) != NULL) {
robj *eleobj = dictGetKey(de);
if ((n = rdbSaveStringObject(rdb,eleobj)) == -1) return -1;
nwritten += n;
}
dictReleaseIterator(di);//释放迭代器
} else if (o->encoding == OBJ_ENCODING_INTSET) {
// 直接将INTSET转换成字符串对象存放
size_t l = intsetBlobLen((intset*)o->ptr);
if ((n = rdbSaveRawString(rdb,o->ptr,l)) == -1) return -1;
nwritten += n;
} else {
serverPanic("Unknown set encoding");
}
} else if (o->type == OBJ_ZSET) {
/* Save a sorted set value */
// 保存有序集合对象值
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
// 采用ziplist编码的情况,直接按照字符串形式存储
size_t l = ziplistBlobLen((unsigned char*)o->ptr);
if ((n = rdbSaveRawString(rdb,o->ptr,l)) == -1) return -1;
nwritten += n;
} else if (o->encoding == OBJ_ENCODING_SKIPLIST) {
// 采用skiplist的情况,遍历所有节点,以次存放
zset *zs = o->ptr;
dictIterator *di = dictGetIterator(zs->dict);
dictEntry *de;
if ((n = rdbSaveLen(rdb,dictSize(zs->dict))) == -1) return -1;
nwritten += n;
//skiplist 编码内部有一个dict结构,存放所有的对象和分值
while((de = dictNext(di)) != NULL) {
robj *eleobj = dictGetKey(de);
double *score = dictGetVal(de);
if ((n = rdbSaveStringObject(rdb,eleobj)) == -1) return -1;
nwritten += n;
if ((n = rdbSaveDoubleValue(rdb,*score)) == -1) return -1;
nwritten += n;
}
dictReleaseIterator(di);
} else {
serverPanic("Unknown sorted set encoding");
}
} else if (o->type == OBJ_HASH) {
/* Save a hash value */
// 存放hash对象值
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
// 采用ziplist编码的情况,直接按照字符串形式存储
size_t l = ziplistBlobLen((unsigned char*)o->ptr);
if ((n = rdbSaveRawString(rdb,o->ptr,l)) == -1) return -1;
nwritten += n;
} else if (o->encoding == OBJ_ENCODING_HT) {
//hash 迭代,依次获取每个key,value
dictIterator *di = dictGetIterator(o->ptr);
dictEntry *de;
if ((n = rdbSaveLen(rdb,dictSize((dict*)o->ptr))) == -1) return -1;
nwritten += n;
//hash中的每个键值对的key值和value值都以字符串对象的形式相邻存储。
while((de = dictNext(di)) != NULL) {
robj *key = dictGetKey(de);
robj *val = dictGetVal(de);
if ((n = rdbSaveStringObject(rdb,key)) == -1) return -1;
nwritten += n;
if ((n = rdbSaveStringObject(rdb,val)) == -1) return -1;
nwritten += n;
}
dictReleaseIterator(di);
} else {
serverPanic("Unknown hash encoding");
}
} else {
serverPanic("Unknown object type");
}
return nwritten;
}
至于string的保存:
/* Like rdbSaveStringObjectRaw() but handle encoded objects */
int rdbSaveStringObject(rio *rdb, robj *obj) {
/* Avoid to decode the object, then encode it again, if the
* object is already integer encoded. */
// 尝试对 INT 编码的字符串进行特殊编码
if (obj->encoding == OBJ_ENCODING_INT) {
return rdbSaveLongLongAsStringObject(rdb,(long)obj->ptr);
} else { // 保存 STRING 编码的字符串
serverAssertWithInfo(NULL,obj,sdsEncodedObject(obj));
return rdbSaveRawString(rdb,obj->ptr,sdslen(obj->ptr));
}
}
好了,跟书上的内容都对应了。接下来看rdbLoad函数。
/* Function called at startup to load RDB or AOF file in memory. */
void loadDataFromDisk(void) {
long long start = ustime();
if (server.aof_state == AOF_ON) {
if (loadAppendOnlyFile(server.aof_filename) == C_OK)
serverLog(LL_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
} else {
if (rdbLoad(server.rdb_filename) == C_OK) {
serverLog(LL_NOTICE,"DB loaded from disk: %.3f seconds",
(float)(ustime()-start)/1000000);
} else if (errno != ENOENT) {
serverLog(LL_WARNING,"Fatal error loading the DB: %s. Exiting.",strerror(errno));
exit(1);
}
}
}
int rdbLoad(char *filename) {
uint32_t dbid;
int type, rdbver;
redisDb *db = server.db+0;
char buf[1024];
long long expiretime, now = mstime();//获取当前load操作的时间
FILE *fp;
rio rdb;
// 只读打开文件
if ((fp = fopen(filename,"r")) == NULL) return C_ERR;
// 初始化一个文件流对象rio且设置对应文件指针
rioInitWithFile(&rdb,fp);
// 设置计算校验和的函数
rdb.update_cksum = rdbLoadProgressCallback;
// 设置载入读或写的最大字节数,2M
rdb.max_processing_chunk = server.loading_process_events_interval_bytes;
// 读出9个字节到buf,buf中保存着Redis版本"redis0007"
if (rioRead(&rdb,buf,9) == 0) goto eoferr;
buf[9] = '\0'; //"redis0007\0"
// 检查版本号
if (memcmp(buf,"REDIS",5) != 0) {
fclose(fp);
serverLog(LL_WARNING,"Wrong signature trying to load DB from file");
errno = EINVAL;
return C_ERR;
}
// 转换成整数检查版本大小
rdbver = atoi(buf+5);
if (rdbver < 1 || rdbver > RDB_VERSION) {
fclose(fp);
serverLog(LL_WARNING,"Can't handle RDB format version %d",rdbver);
errno = EINVAL;
return C_ERR;
}
// 设置载入时server的状态信息
startLoading(fp);
while(1) {// 开始读取RDB文件到数据库中
robj *key, *val;
expiretime = -1;
/* Read type.
* 读入类型指示,决定该如何读入之后跟着的数据。
*
* 这个指示可以是 rdb.h 中定义的所有以
* REDIS_RDB_TYPE_* 为前缀的常量的其中一个
* 或者所有以 REDIS_RDB_OPCODE_* 为前缀的常量的其中一个
*/
if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;
/* Handle special types. */
// 读入过期时间值
if (type == RDB_OPCODE_EXPIRETIME) {
/* EXPIRETIME: load an expire associated with the next key
* to load. Note that after loading an expire we need to
* load the actual type, and continue. */
// 以秒计算的过期时间
if ((expiretime = rdbLoadTime(&rdb)) == -1) goto eoferr;
/* We read the time so we need to read the object type again. */
// 从过期时间后读出一个键值对的类型
if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;
/* the EXPIRETIME opcode specifies time in seconds, so convert
* into milliseconds. */
expiretime *= 1000;//转换成毫秒
} else if (type == RDB_OPCODE_EXPIRETIME_MS) {
// 以毫秒计算的过期时间
/* EXPIRETIME_MS: milliseconds precision expire times introduced
* with RDB v3. Like EXPIRETIME but no with more precision. */
if ((expiretime = rdbLoadMillisecondTime(&rdb)) == -1) goto eoferr;
/* We read the time so we need to read the object type again. */
//在过期时间之后会跟着一个键值对,我们要读入这个键值对的类型
if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;
} else if (type == RDB_OPCODE_EOF) {
// 如果读到EOF,则直接跳出循环
/* EOF: End of file, exit the main loop. */
break;
} else if (type == RDB_OPCODE_SELECTDB) {
// 读出的是切换数据库操作
/* SELECTDB: Select the specified database. */
// 读取出一个长度,保存的是数据库的ID
if ((dbid = rdbLoadLen(&rdb,NULL)) == RDB_LENERR)
goto eoferr;
// 检查读出的ID是否合法
if (dbid >= (unsigned)server.dbnum) {
serverLog(LL_WARNING,
"FATAL: Data file was created with a Redis "
"server configured to handle more than %d "
"databases. Exiting\n", server.dbnum);
exit(1);
}
// 在程序内容切换数据库
db = server.db+dbid;
continue; /* Read type again. */
} else if (type == RDB_OPCODE_RESIZEDB) {
// 如果读出调整哈希表的操作
/* RESIZEDB: Hint about the size of the keys in the currently
* selected data base, in order to avoid useless rehashing. */
uint32_t db_size, expires_size;
// 读出一个数据库键值对字典的大小
if ((db_size = rdbLoadLen(&rdb,NULL)) == RDB_LENERR)
goto eoferr;
// 读出一个数据库过期字典的大小
if ((expires_size = rdbLoadLen(&rdb,NULL)) == RDB_LENERR)
goto eoferr;
// 扩展两个字典
dictExpand(db->dict,db_size);
dictExpand(db->expires,expires_size);
continue; /* Read type again. */
} else if (type == RDB_OPCODE_AUX) {
// 读出的是一个辅助字段
/* AUX: generic string-string fields. Use to add state to RDB
* which is backward compatible. Implementations of RDB loading
* are requierd to skip AUX fields they don't understand.
*
* An AUX field is composed of two strings: key and value. */
robj *auxkey, *auxval;
// 读出辅助字段的键对象和值对象
if ((auxkey = rdbLoadStringObject(&rdb)) == NULL) goto eoferr;
if ((auxval = rdbLoadStringObject(&rdb)) == NULL) goto eoferr;
// 键对象的第一个字符是%
if (((char*)auxkey->ptr)[0] == '%') {
/* All the fields with a name staring with '%' are considered
* information fields and are logged at startup with a log
* level of NOTICE. */
serverLog(LL_NOTICE,"RDB '%s': %s",
(char*)auxkey->ptr,
(char*)auxval->ptr);
} else {
/* We ignore fields we don't understand, as by AUX field
* contract. */
serverLog(LL_DEBUG,"Unrecognized RDB AUX field: '%s'",
(char*)auxkey->ptr);
}
decrRefCount(auxkey);
decrRefCount(auxval);
continue; /* Read type again. */
}
/* Read key */
// 读出一个key对象
if ((key = rdbLoadStringObject(&rdb)) == NULL) goto eoferr;
/* Read value */
// 读出一个val对象
if ((val = rdbLoadObject(type,&rdb)) == NULL) goto eoferr;
/* Check if the key already expired. This function is used when loading
* an RDB file from disk, either at startup, or when an RDB was
* received from the master. In the latter case, the master is
* responsible for key expiry. If we would expire keys here, the
* snapshot taken by the master may not be reflected on the slave. */
// 如果当前环境不是从节点,且该键设置了过期时间,已经过期
if (server.masterhost == NULL && expiretime != -1 && expiretime < now) {
// 释放键值对
decrRefCount(key);
decrRefCount(val);
continue;//跳过
}
/* Add the new object in the hash table
* 将键值对关联到数据库中
*/
dbAdd(db,key,val);
/* Set the expire time if needed
*设置过期时间
*/
if (expiretime != -1) setExpire(db,key,expiretime);
decrRefCount(key); //释放临时对象
}
/* Verify the checksum if RDB version is >= 5 */
// 当RDB版本大于5时,且开启了校验和的功能,那么进行校验和
if (rdbver >= 5 && server.rdb_checksum) {
uint64_t cksum, expected = rdb.cksum;
// 读入文件的校验和
if (rioRead(&rdb,&cksum,8) == 0) goto eoferr;
memrev64ifbe(&cksum);
// 比对校验和
if (cksum == 0) {
serverLog(LL_WARNING,"RDB file was saved with checksum disabled: no check performed.");
} else if (cksum != expected) {
serverLog(LL_WARNING,"Wrong RDB checksum. Aborting now.");
rdbExitReportCorruptRDB("RDB CRC error");
}
}
// 关闭 RDB
fclose(fp);
stopLoading(); // 服务器从载入状态中退出
return C_OK;
// 错误退出
eoferr: /* unexpected end of file is handled here with a fatal exit */
serverLog(LL_WARNING,"Short read or OOM loading DB. Unrecoverable error, aborting now.");
rdbExitReportCorruptRDB("Unexpected EOF reading RDB file");
return C_ERR; /* Just to avoid warning */
}
/* Use rdbLoadType() to load a TYPE in RDB format, but returns -1 if the
* type is not specifically a valid Object Type. */
int rdbLoadObjectType(rio *rdb) {
int type;
if ((type = rdbLoadType(rdb)) == -1) return -1;
if (!rdbIsObjectType(type)) return -1;
return type;
}
load 跟save是想反的过程,就不在朱行注释了
/* Load a Redis object of the specified type from the specified file.
* On success a newly allocated object is returned, otherwise NULL. */
robj *rdbLoadObject(int rdbtype, rio *rdb) {
robj *o = NULL, *ele, *dec;
size_t len;
unsigned int i;
if (rdbtype == RDB_TYPE_STRING) {
/* Read string value */
if ((o = rdbLoadEncodedStringObject(rdb)) == NULL) return NULL;
o = tryObjectEncoding(o);
} else if (rdbtype == RDB_TYPE_LIST) {
/* Read list value */
if ((len = rdbLoadLen(rdb,NULL)) == RDB_LENERR) return NULL;
o = createQuicklistObject();
quicklistSetOptions(o->ptr, server.list_max_ziplist_size,
server.list_compress_depth);
/* Load every single element of the list */
while(len--) {
if ((ele = rdbLoadEncodedStringObject(rdb)) == NULL) return NULL;
dec = getDecodedObject(ele);
size_t len = sdslen(dec->ptr);
quicklistPushTail(o->ptr, dec->ptr, len);
decrRefCount(dec);
decrRefCount(ele);
}
} else if (rdbtype == RDB_TYPE_SET) {
/* Read list/set value */
if ((len = rdbLoadLen(rdb,NULL)) == RDB_LENERR) return NULL;
/* Use a regular set when there are too many entries. */
if (len > server.set_max_intset_entries) {
o = createSetObject();
/* It's faster to expand the dict to the right size asap in order
* to avoid rehashing */
if (len > DICT_HT_INITIAL_SIZE)
dictExpand(o->ptr,len);
} else {
o = createIntsetObject();
}
/* Load every single element of the list/set */
for (i = 0; i < len; i++) {
long long llval;
if ((ele = rdbLoadEncodedStringObject(rdb)) == NULL) return NULL;
ele = tryObjectEncoding(ele);
if (o->encoding == OBJ_ENCODING_INTSET) {
/* Fetch integer value from element */
if (isObjectRepresentableAsLongLong(ele,&llval) == C_OK) {
o->ptr = intsetAdd(o->ptr,llval,NULL);
} else {
setTypeConvert(o,OBJ_ENCODING_HT);
dictExpand(o->ptr,len);
}
}
/* This will also be called when the set was just converted
* to a regular hash table encoded set */
if (o->encoding == OBJ_ENCODING_HT) {
dictAdd((dict*)o->ptr,ele,NULL);
} else {
decrRefCount(ele);
}
}
} else if (rdbtype == RDB_TYPE_ZSET) {
/* Read list/set value */
size_t zsetlen;
size_t maxelelen = 0;
zset *zs;
if ((zsetlen = rdbLoadLen(rdb,NULL)) == RDB_LENERR) return NULL;
o = createZsetObject();
zs = o->ptr;
/* Load every single element of the list/set */
while(zsetlen--) {
robj *ele;
double score;
zskiplistNode *znode;
if ((ele = rdbLoadEncodedStringObject(rdb)) == NULL) return NULL;
ele = tryObjectEncoding(ele);
if (rdbLoadDoubleValue(rdb,&score) == -1) return NULL;
/* Don't care about integer-encoded strings. */
if (sdsEncodedObject(ele) && sdslen(ele->ptr) > maxelelen)
maxelelen = sdslen(ele->ptr);
znode = zslInsert(zs->zsl,score,ele);
dictAdd(zs->dict,ele,&znode->score);
incrRefCount(ele); /* added to skiplist */
}
/* Convert *after* loading, since sorted sets are not stored ordered. */
if (zsetLength(o) <= server.zset_max_ziplist_entries &&
maxelelen <= server.zset_max_ziplist_value)
zsetConvert(o,OBJ_ENCODING_ZIPLIST);
} else if (rdbtype == RDB_TYPE_HASH) {
size_t len;
int ret;
len = rdbLoadLen(rdb, NULL);
if (len == RDB_LENERR) return NULL;
o = createHashObject();
/* Too many entries? Use a hash table. */
if (len > server.hash_max_ziplist_entries)
hashTypeConvert(o, OBJ_ENCODING_HT);
/* Load every field and value into the ziplist */
while (o->encoding == OBJ_ENCODING_ZIPLIST && len > 0) {
robj *field, *value;
len--;
/* Load raw strings */
field = rdbLoadStringObject(rdb);
if (field == NULL) return NULL;
serverAssert(sdsEncodedObject(field));
value = rdbLoadStringObject(rdb);
if (value == NULL) return NULL;
serverAssert(sdsEncodedObject(value));
/* Add pair to ziplist */
o->ptr = ziplistPush(o->ptr, field->ptr, sdslen(field->ptr), ZIPLIST_TAIL);
o->ptr = ziplistPush(o->ptr, value->ptr, sdslen(value->ptr), ZIPLIST_TAIL);
/* Convert to hash table if size threshold is exceeded */
if (sdslen(field->ptr) > server.hash_max_ziplist_value ||
sdslen(value->ptr) > server.hash_max_ziplist_value)
{
decrRefCount(field);
decrRefCount(value);
hashTypeConvert(o, OBJ_ENCODING_HT);
break;
}
decrRefCount(field);
decrRefCount(value);
}
/* Load remaining fields and values into the hash table */
while (o->encoding == OBJ_ENCODING_HT && len > 0) {
robj *field, *value;
len--;
/* Load encoded strings */
field = rdbLoadEncodedStringObject(rdb);
if (field == NULL) return NULL;
value = rdbLoadEncodedStringObject(rdb);
if (value == NULL) return NULL;
field = tryObjectEncoding(field);
value = tryObjectEncoding(value);
/* Add pair to hash table */
ret = dictAdd((dict*)o->ptr, field, value);
if (ret == DICT_ERR) {
rdbExitReportCorruptRDB("Duplicate keys detected");
}
}
/* All pairs should be read by now */
serverAssert(len == 0);
} else if (rdbtype == RDB_TYPE_LIST_QUICKLIST) {
if ((len = rdbLoadLen(rdb,NULL)) == RDB_LENERR) return NULL;
o = createQuicklistObject();
quicklistSetOptions(o->ptr, server.list_max_ziplist_size,
server.list_compress_depth);
while (len--) {
unsigned char *zl = rdbGenericLoadStringObject(rdb,RDB_LOAD_PLAIN);
if (zl == NULL) return NULL;
quicklistAppendZiplist(o->ptr, zl);
}
} else if (rdbtype == RDB_TYPE_HASH_ZIPMAP ||
rdbtype == RDB_TYPE_LIST_ZIPLIST ||
rdbtype == RDB_TYPE_SET_INTSET ||
rdbtype == RDB_TYPE_ZSET_ZIPLIST ||
rdbtype == RDB_TYPE_HASH_ZIPLIST)
{
unsigned char *encoded = rdbGenericLoadStringObject(rdb,RDB_LOAD_PLAIN);
if (encoded == NULL) return NULL;
o = createObject(OBJ_STRING,encoded); /* Obj type fixed below. */
/* Fix the object encoding, and make sure to convert the encoded
* data type into the base type if accordingly to the current
* configuration there are too many elements in the encoded data
* type. Note that we only check the length and not max element
* size as this is an O(N) scan. Eventually everything will get
* converted. */
switch(rdbtype) {
case RDB_TYPE_HASH_ZIPMAP:
/* Convert to ziplist encoded hash. This must be deprecated
* when loading dumps created by Redis 2.4 gets deprecated. */
{
unsigned char *zl = ziplistNew();
unsigned char *zi = zipmapRewind(o->ptr);
unsigned char *fstr, *vstr;
unsigned int flen, vlen;
unsigned int maxlen = 0;
while ((zi = zipmapNext(zi, &fstr, &flen, &vstr, &vlen)) != NULL) {
if (flen > maxlen) maxlen = flen;
if (vlen > maxlen) maxlen = vlen;
zl = ziplistPush(zl, fstr, flen, ZIPLIST_TAIL);
zl = ziplistPush(zl, vstr, vlen, ZIPLIST_TAIL);
}
zfree(o->ptr);
o->ptr = zl;
o->type = OBJ_HASH;
o->encoding = OBJ_ENCODING_ZIPLIST;
if (hashTypeLength(o) > server.hash_max_ziplist_entries ||
maxlen > server.hash_max_ziplist_value)
{
hashTypeConvert(o, OBJ_ENCODING_HT);
}
}
break;
case RDB_TYPE_LIST_ZIPLIST:
o->type = OBJ_LIST;
o->encoding = OBJ_ENCODING_ZIPLIST;
listTypeConvert(o,OBJ_ENCODING_QUICKLIST);
break;
case RDB_TYPE_SET_INTSET:
o->type = OBJ_SET;
o->encoding = OBJ_ENCODING_INTSET;
if (intsetLen(o->ptr) > server.set_max_intset_entries)
setTypeConvert(o,OBJ_ENCODING_HT);
break;
case RDB_TYPE_ZSET_ZIPLIST:
o->type = OBJ_ZSET;
o->encoding = OBJ_ENCODING_ZIPLIST;
if (zsetLength(o) > server.zset_max_ziplist_entries)
zsetConvert(o,OBJ_ENCODING_SKIPLIST);
break;
case RDB_TYPE_HASH_ZIPLIST:
o->type = OBJ_HASH;
o->encoding = OBJ_ENCODING_ZIPLIST;
if (hashTypeLength(o) > server.hash_max_ziplist_entries)
hashTypeConvert(o, OBJ_ENCODING_HT);
break;
default:
rdbExitReportCorruptRDB("Unknown RDB encoding type %d",rdbtype);
break;
}
} else {
rdbExitReportCorruptRDB("Unknown RDB encoding type %d",rdbtype);
}
return o;
}
参考: