RDB是redis的另一种持久化方式,相当于是定时快照,也用于主从同步中快照+redo log。redis在进行RDB时,不需要加锁,这是通过利用父子进程共享同一份内存完成的。在父进程fork子进程之后,父子以copy-on-write方式共享同一份物理内存,当两个进程写内存时,才会按照内存页复制内存。这就需要保证在RDB时,最坏情况下需要保证有2倍的内存空间用于父子进程使用(redis使用时占用1G,那么就要保证系统有2G的内存,否则可能会出现使用swap的情况)。因为copy-on-write,所以需要避免不必要的内存拷贝。子进程中基本上只需要读内存,而父进程响应客户端请求,就需要修改内存,为了减少内存修改,父进程会暂停keyspace对应的hash表的rehash(rehash会有大量拷贝,要在不同的桶之间拷贝数据)。下面看一下RDB相关内容。
RDB文件格式比较简单,可以看做是一条条指令序列,每条指令的组成:
|-----------------------|----------------------------------|
| OP code: 1Byte | Instruction: nBytes |
|-----------------------|----------------------------------|
在加载RDB时,就是对这个指令序列进行解析。所有的OP code包括:
REDIS_RDB_OPCODE_EXPIRETIME_MS: ms级的过期时间
REDIS_RDB_OPCODE_EXPIRETIME:秒级的过期时间
REDIS_RDB_OPCODE_SELECTDB:用于select db命令
REDIS_RDB_OPCODE_EOF:RDB文件结尾
OP code还包括所有的数据类型(REDIS_RDB_TYPE_LIST, REDIS_RDB_TYPE_SET等),用于指定后续kv对中,value的类型。
RDB文件的前5个字节是magic number,用于表示文件是RDB文件。接下来的4个字节是版本号,在加载RDB时,会根据RDB的版本号和redis的版本号比较,查看是否可以处理该版本的RDB。然后,是一条条指令序列,最后以EOF结尾。
首先看一下dump的时机,主要分为3块:
1)save命令:客户端发送save命令,redis实例阻塞执行dump。在saveCommand函数中。
2)bgsave命令:dump任务由子进程完成,主进程可以继续服务请求。在bgsaveCommand函数中。
3)被动触发:redis变更次数或者dump的间隔超过阈值。在serverCron中,检测并触发。
4)主从同步触发:在不能实现partial sync时,master需要将rdb传输给slave。在syncCommand函数中。
下面看一下rdb dump的具体过程,这是由rdbSave函数完成的。
//
// 创建并打开临时rdb文件
//
snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
fp = fopen(tmpfile,"w");
if (!fp) {
redisLog(REDIS_WARNING, "Failed opening .rdb for saving: %s",
strerror(errno));
return REDIS_ERR;
}
rioInitWithFile(&rdb,fp);
if (server.rdb_checksum)
rdb.update_cksum = rioGenericUpdateChecksum;
创建并打开临时文件,这是为了保证rdb的数据完整性,只有在dump成功后,才会替换原文件。然后是初始化rio,用于输出。
//
// 写入magic number,format:
// 9bit: REDIS[RDB_VERSION]
//
snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);
if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr;
写入magic number以及版本号。
接下来是一个循环,用于对每个redis DB遍历,并生成对应的内容。
for (j = 0; j < server.dbnum; j++) {
// dump该DB
}
看下每个DB的dump过程,实际上就是遍历并输出每个Key-Value对。
redisDb *db = server.db+j;
dict *d = db->dict;
if (dictSize(d) == 0) continue;
di = dictGetSafeIterator(d);
if (!di) {
fclose(fp);
return REDIS_ERR;
}
先判断DB是否为空,如果为空就跳过。然后获取DB的迭代器。
/* Write the SELECT DB opcode */
if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;
if (rdbSaveLen(&rdb,j) == -1) goto werr;
输出select db的opcode,然后是对应的DB号。具体格式是,1字节的OPcode,加上1,2或4字节的DB号。
/* Iterate this DB writing every entry */
while((de = dictNext(di)) != NULL) {
sds keystr = dictGetKey(de);
robj key, *o = dictGetVal(de);
long long expire;
initStaticStringObject(key,keystr);
expire = getExpire(db,&key);
if (rdbSaveKeyValuePair(&rdb,&key,o,expire,now) == -1) goto werr;
}
dictReleaseIterator(di);
然后是一个while循环,遍历所有K-V对,并进行dump。对于每个KV,获取key,value和expire time。然后调用rdbSaveKeyValuePair函数进行dump,下面就看一下这个函数。
/* Save a key-value pair, with expire time, type, key, value.
* 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,REDIS_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;
}
如果expire不为空,则输出该信息。先写出OPCode表示expire,然后是具体的超时时间。接下来是具体的KV对,首先也类似OPCode,表示value的类型,然后是字符串类型的key,最后是value对象。具体对象的dump内容比较多,这里暂时不展开。
上面完成所有DB的dump后,接下来看一下收尾工作。
di = NULL; /* So that we don't release it again on error. */
/* EOF opcode */
if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;
输出EOF对应的OPCode,表示rdb结束。
/* CRC64 checksum. It will be zero if checksum computation is disabled, the
* loading code skips the check in this case. */
cksum = rdb.cksum;
memrev64ifbe(&cksum);
if (rioWrite(&rdb,&cksum,8) == 0) goto werr;
计算并输出CRC。
/* Make sure data will not remain on the OS's output buffers */
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
if (fclose(fp) == EOF) goto werr;
首先是调用fflush将输出缓冲区刷新到page cache,然后调用fsync将cache中的内容写盘,最后关闭文件。
/* Use RENAME to make sure the DB file is changed atomically only
* if the generate DB file is ok. */
if (rename(tmpfile,filename) == -1) {
redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));
unlink(tmpfile);
return REDIS_ERR;
}
将临时文件重命名为指定的文件名。
redisLog(REDIS_NOTICE,"DB saved on disk");
server.dirty = 0;
server.lastsave = time(NULL);
server.lastbgsave_status = REDIS_OK;
return REDIS_OK;
最后,打印日志,重置dirty和lastsave,这两个值会影响被动触发rdb dump的时机。
werr:
fclose(fp);
unlink(tmpfile);
redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));
if (di) dictReleaseIterator(di);
return REDIS_ERR;
上述出错的错误处理,主要就是删除临时文件,销毁迭代器并打印日志。
上面就是整个rdb dump的过程,在后台进行rdb dump时,上述是在子进程中完成的,主进程还需要进行最后的一些清理工作,下面看一下这个部分。在serverCron中,如果server.rdb_child_pid不为-1(存在rdb dump的子进程),会调用wait3对子进程收割,如果是rdb子进程完成,会调用backgroundSaveDoneHandler函数做最后处理。
/* A background saving child (BGSAVE) terminated its work. Handle this. */
void backgroundSaveDoneHandler(int exitcode, int bysignal) {
if (!bysignal && exitcode == 0) {
redisLog(REDIS_NOTICE,
"Background saving terminated with success");
server.dirty = server.dirty - server.dirty_before_bgsave;
server.lastsave = time(NULL);
server.lastbgsave_status = REDIS_OK;
} else if (!bysignal && exitcode != 0) {
redisLog(REDIS_WARNING, "Background saving error");
server.lastbgsave_status = REDIS_ERR;
} else {
mstime_t latency;
redisLog(REDIS_WARNING,
"Background saving terminated by signal %d", bysignal);
latencyStartMonitor(latency);
rdbRemoveTempFile(server.rdb_child_pid);
latencyEndMonitor(latency);
latencyAddSampleIfNeeded("rdb-unlink-temp-file",latency);
/* SIGUSR1 is whitelisted, so we have a way to kill a child without
* tirggering an error conditon. */
if (bysignal != SIGUSR1)
server.lastbgsave_status = REDIS_ERR;
}
server.rdb_child_pid = -1;
server.rdb_save_time_last = time(NULL)-server.rdb_save_time_start;
server.rdb_save_time_start = -1;
/* Possibly there are slaves waiting for a BGSAVE in order to be served
* (the first stage of SYNC is a bulk transfer of dump.rdb) */
updateSlavesWaitingBgsave((!bysignal && exitcode == 0) ? REDIS_OK : REDIS_ERR);
}
相对于aof rewrite,这块工作要简单一些,主要是根据子进程的退出状态以及是否被信号kill进行处理。最后一个函数updateSlavesWaitingBgsave是用于在主从同步中,完成rdb dump,通知向slave传输rdb。
RDB加载主要是两个地方会用到:
1)redis启动时,加载RDB
2)主从同步时,master向从发送RDB
redis启动时加载RDB,和AOF一样,是在loadDataFromDisk函数:
/* Function called at startup to load RDB or AOF file in memory. */
void loadDataFromDisk(void) {
long long start = ustime();
if (server.aof_state == REDIS_AOF_ON) {
if (loadAppendOnlyFile(server.aof_filename) == REDIS_OK)
redisLog(REDIS_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
} else {
if (rdbLoad(server.rdb_filename) == REDIS_OK) {
redisLog(REDIS_NOTICE,"DB loaded from disk: %.3f seconds",
(float)(ustime()-start)/1000000);
} else if (errno != ENOENT) {
redisLog(REDIS_WARNING,"Fatal error loading the DB: %s. Exiting.",strerror(errno));
exit(1);
}
}
}
如果没有开启AOF配置,那么就会尝试加载RDB。由rdbLoad完成RDB的加载,下面看一下这个函数。
uint32_t dbid;
int type, rdbver;
redisDb *db = server.db+0;
char buf[1024];
long long expiretime, now = mstime();
FILE *fp;
rio rdb;
if ((fp = fopen(filename,"r")) == NULL) return REDIS_ERR;
首先是打开RDB文件。
rioInitWithFile(&rdb,fp);
rdb.update_cksum = rdbLoadProgressCallback;
rdb.max_processing_chunk = server.loading_process_events_interval_bytes;
初始化rio,设置update_cksum回调函数,以及read的块大小(默认配置的是2M)。这里使用update_cksum还完成一些功能:
1)更新加载进度
2)如果是主从同步过程中,加载RDB,因为整个加载过程可能会很漫长,所以需要不停的想master发送心跳,避免master认为这个slave已经timeout,主动断开连接。
3)处理一些io事件
if (rioRead(&rdb,buf,9) == 0) goto eoferr;
buf[9] = '\0';
if (memcmp(buf,"REDIS",5) != 0) {
fclose(fp);
redisLog(REDIS_WARNING,"Wrong signature trying to load DB from file");
errno = EINVAL;
return REDIS_ERR;
}
rdbver = atoi(buf+5);
if (rdbver < 1 || rdbver > REDIS_RDB_VERSION) {
fclose(fp);
redisLog(REDIS_WARNING,"Can't handle RDB format version %d",rdbver);
errno = EINVAL;
return REDIS_ERR;
}
读取RDB前9个字节,校验magic number以及版本号。
startLoading(fp);
准备开始加载,记录加载开始时间,以及需要加载的字节总数,用于更新加载进度。
接下来是一个解释循环,不停的读取一条条指令。
while (1) {
// 解释一条条指令
}
下面看一下一条指令的解释过程:
robj *key, *val;
expiretime = -1;
/* Read type. */
if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;
首先读取type(对应OPCode)。
if (type == REDIS_RDB_OPCODE_EXPIRETIME) {
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 == REDIS_RDB_OPCODE_EXPIRETIME_MS) {
/* Milliseconds precision expire times introduced with RDB
* version 3. */
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;
}
如果OPCode对应的expire指令,需要解析出对应的expire time,然后再次读取type(对应随后的kv对中value的类型)。
if (type == REDIS_RDB_OPCODE_EOF)
break;
如果OPCode对应的是EOF指令,则RDB加载完成,跳出循环。
/* Handle SELECT DB opcode as a special case */
if (type == REDIS_RDB_OPCODE_SELECTDB) {
if ((dbid = rdbLoadLen(&rdb,NULL)) == REDIS_RDB_LENERR)
goto eoferr;
if (dbid >= (unsigned)server.dbnum) {
redisLog(REDIS_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;
}
如果OPCode是selectDB指令,读取DB号,然后切换到对应的DB。
上面把特殊的指令执行完毕,接下来要解析KV对。
/* Read key */
if ((key = rdbLoadStringObject(&rdb)) == NULL) goto eoferr;
读取字符串类型的key。
/* Read value */
if ((val = rdbLoadObject(type,&rdb)) == NULL) goto eoferr;
读取value,函数rdbLoadObject会根据type的不同,执行不同类型对象的加载,这里不对该函数展开。
/* 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;
}
检测是否过期,如果过期就不会将KV对添加到DB中。
/* Add the new object in the hash table */
dbAdd(db,key,val);
将KV对添加到DB。
/* Set the expire time if needed */
if (expiretime != -1) setExpire(db,key,expiretime);
decrRefCount(key);
如果设置了expire time,则添加到expire dict中。
/* Verify the checksum if RDB version is >= 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) {
redisLog(REDIS_WARNING,"RDB file was saved with checksum disabled: no check performed.");
} else if (cksum != expected) {
redisLog(REDIS_WARNING,"Wrong RDB checksum. Aborting now.");
exit(1);
}
}
检查check sum。
fclose(fp);
stopLoading();
return REDIS_OK;
最后,关闭文件,并将server.load置为0,表示加载不在进行。
eoferr: /* unexpected end of file is handled here with a fatal exit */
redisLog(REDIS_WARNING,"Short read or OOM loading DB. Unrecoverable error, aborting now.");
exit(1);
return REDIS_ERR; /* Just to avoid warning */
在上述加载过程中出错,会跳到eoferr分支。加载出错时,打印日志并退出进程。