RDB是redis的另一种持久化方式,相当于是定时快照,也用于主从同步中快照+redo log。redis在进行RDB时,不需要加锁,这是通过利用父子进程共享同一份内存完成的。在父进程fork子进程之后,父子以copy-on-write方式共享同一份物理内存,当两个进程写内存时,才会按照内存页复制内存。这就需要保证在RDB时,最坏情况下需要保证有2倍的内存空间用于父子进程使用(redis使用时占用1G,那么就要保证系统有2G的内存,否则可能会出现使用swap的情况)。因为copy-on-write,所以需要避免不必要的内存拷贝。子进程中基本上只需要读内存,而父进程响应客户端请求,就需要修改内存,为了减少内存修改,父进程会暂停keyspace对应的hash表的rehash(rehash会有大量拷贝,要在不同的桶之间拷贝数据)。下面看一下RDB相关内容。
1. 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结尾。
2. RDB dump
首先看一下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函数完成的。
// <MM>
// 创建并打开临时rdb文件
// </MM>
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,用于输出。
// <MM>
// 写入magic number,format:
// 9bit: REDIS[RDB_VERSION]
// </MM>
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)