详解 Redis 持久化之 RDB
本文带大家了解一下
Redis
数据一种持久化方式RDB
的实现。包括Redis
内存快照RDB
⽂件的创建时机以及⽣成⽅法。可以让你掌握RDB
⽂件的格式,学习如何制作数据库镜像。
RDB 创建的入口函数
Redis
创建 RDB
文件的函数有三个,分别是 rdbSave
, rdbSaveBackground
, rdbSaveToSlavesSockets
这三个函数。
rdbSave
rdbSave
是 Redis
在本地磁盘创建 RDB
⽂件的入口函数。它对应了 Redis
的 save
命令,会在 save
命令的实现函数 saveCommand
中被调用,这个命令是使用主线程执行的,会阻塞其他命令的执行。rdbSave
函数最终会调用 rdbSaveRio
函数来实际创建RDB⽂件。
rdbSaveBackground
rdbSaveBackground
是 Redis
使⽤⼦进程方式在本地磁盘创建 RDB
⽂件的入口函数。它对应了 Redis
的 bgsave
命令,会在 bgsave
命令的实现函数 bgsaveCommand
中被调⽤。这个函数会调⽤ fork
创建 ⼀个⼦进程,让⼦进程调用 rdbSave
函数来创建 RDB
⽂件,而主线程本⾝可以继续处理客户端请求。
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
...
if ((childpid = fork()) == 0) {
// 子进程执行方法
...
// 调用 rdbSave 创建 RDB 文件
retval = rdbSave(filename,rsi);
...
// 子进程退出
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
/* Parent */
// 父进程也就是主线程执行方法
...
return C_OK;
}
return C_OK; /* unreached */
}
rdbSaveToSlavesSockets
rdbSaveToSlavesSockets
函数是 Redis
采用不落盘方式传输 RDB
文件进行主从复制时,创建 RDB
文件的入口函数。
与 rdbSaveBackground
函数类似,rdbSaveToSlavesSockets
也是通过创建子进程,让子进程创建 RDB
文件。与 rdbSaveBackground
不同的是,rdbSaveToSlavesSockets
是通过网络以字节流的形式直接发送 RDB
文件的二进制文件数据给从节点。
RDB 创建时机
从上面的分析中我们知道 RDB
文件创建的三个时机分别是执行 save
命令、执行 bgsave
命令和进行主从复制。除了这几个时机还有哪些地方会触发 RDB
文件的创建呢?接下来我们将分析一下其他的创建时机。
rdbSave
通过查找 rdbSave
函数的调用,我们发现在 db.c
文件中的 flushallCommand
函数和 server.c
文件中的 prepareForShutdown
函数会调用 rdbSave
函数,也就是说在 Redis
执行 flushall
命令或 Redis
正常关闭时会创建 RDB
文件。
rdbSaveBackground
通过查找rdbSaveBackground
函数的调用,我们发现在 replication.c
中的 startBgsaveForReplication
函数和 server.c
文件中的 serverCron
函数会调用 rdbSaveBackground
函数,也就是说在主从复制以及定时任务按周期会调用 rdbSaveBackground
来创建 RDB
文件。
serverCron
函数会在下面两种情况下调用 rdbSaveBackground
生成 RDB
文件。
- 满足配置的定时生成
RDB
文件的配置时。 bgsave
因为AOF
重写导致bgsave
被迫推迟时。
可见 RDB
文件只是周期性的保存某一时刻的数据。
rdbSaveToSlavesSockets
过查找rdbSaveToSlavesSockets
函数的调用,我们发现只有在 replication.c
中的 startBgsaveForReplication
函数会被调用,而 startBgsaveForReplication
函数被 replication.c
⽂件中的 syncCommand
函数和 replicationCron
函数调⽤,也就是说 Redis
执行主从复制命令以及周期性检测主从复制状态时会触发 RDB
⽣成。为了让从节点能够识别⽤来同步数据的 RDB
内容,rdbSaveToSlavesSockets
函数调⽤ rdbSaveRioWithEOFMark
函数在 RDB
⼆进制数据的前后加上了标识字符串,我们来看下代码:
#define RDB_EOF_MARK_SIZE 40
int rdbSaveRioWithEOFMark(rio *rdb, int *error, rdbSaveInfo *rsi) {
char eofmark[RDB_EOF_MARK_SIZE];
// 生成随机成 40 字节的 16 进制字符串,保存在 eofmark 中
getRandomHexChars(eofmark,RDB_EOF_MARK_SIZE);
if (error) *error = 0;
// 写入 $EOF:
if (rioWrite(rdb,"$EOF:",5) == 0) goto werr;
// 写入 eofmark
if (rioWrite(rdb,eofmark,RDB_EOF_MARK_SIZE) == 0) goto werr;
// 写入 \r\n
if (rioWrite(rdb,"\r\n",2) == 0) goto werr;
// 写入 rdb 中的数据
if (rdbSaveRio(rdb,error,RDB_SAVE_NONE,rsi) == C_ERR) goto werr;
// 再次写入 eofmark
if (rioWrite(rdb,eofmark,RDB_EOF_MARK_SIZE) == 0) goto werr;
return C_OK;
werr: /* Write error. */
/* Set 'error' only if not already set by rdbSaveRio() call. */
if (error && *error == 0) *error = errno;
return C_ERR;
}
新增的标识字符串如下图所示:
好了到这里我们找到了所有 RDB
创建的时机,下面这张图展示了函数的调用关系。
RDB 文件
一个 RDB
文件是主要由三部分组成的。
- 文件头: 这部分内容保存了
Redis
的魔数、RDB
版本、Redis
版本、RDB
⽂件创建时间、键值对占⽤的内存大小等信息。 - 文件数据: 这部分保存了
Redis
数据库实际的所有键值对。 - 文件尾: 这部分保存了
RDB
⽂件的结束标识符,以及整个⽂件的校验值。用于校验文件是否被篡改。RDB
文件组成如下图所示:
真正创建
RDB
文件的函数是 rdbSaveRio
,下面我们通过 rdbSaveRio
函数分别看文件的这三部分具体实现。
文件头
rdbSaveRio
首先会将魔数写入 RDB
文件,当在 RDB
⽂件头中写⼊魔数后,rdbSaveRio
函数紧接着会调⽤ rdbSaveInfoAuxFields
函数将和 Redis server
相关的⼀些属性信息写⼊ RDB
⽂件头。
...
// 生成魔数 magic
snprintf(magic,sizeof(magic),"REDIS%04d",RDB_VERSION);
// 将魔数写到 RDB 中
if (rdbWriteRaw(rdb,magic,9) == -1) goto werr;
// 写入属性信息
if (rdbSaveInfoAuxFields(rdb,flags,rsi) == -1) goto werr;
if (rdbSaveModulesAux(rdb, REDISMODULE_AUX_BEFORE_RDB) == -1) goto werr;
...
文件数据
Redis Server
中会有多个数据库,rdbSaveRio
会遍历所有的数据库,并将里面的数据写入到 RDB
文件中。我们看一下代码实现:
...
for (j = 0; j < server.dbnum; j++) {
...
// 写入 SELECTDB 操作符
if (rdbSaveType(rdb,RDB_OPCODE_SELECTDB) == -1) goto werr;
// 写入数据库编号
if (rdbSaveLen(rdb,j) == -1) goto werr;
uint64_t db_size, expires_size;
// 获取全局哈希表大小
db_size = dictSize(db->dict);
// 获取过期键哈希表大小
expires_size = dictSize(db->expires);
// 写入 RESIZEDB 操作符
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;
// 遍历所有的键值对
while((de = dictNext(di)) != NULL) {
// 获取键
sds keystr = dictGetKey(de);
// 获取值对象
robj key, *o = dictGetVal(de);
long long expire;
// 将 key 从 sds 类型转换为 robj
initStaticStringObject(key,keystr);
// 获取键的过期时间
expire = getExpire(db,&key);
// 将键、值以及过期时间写入 RDB 文件
if (rdbSaveKeyValuePair(rdb,&key,o,expire) == -1) goto werr;
/* When this RDB is produced as part of an AOF rewrite, move
* accumulated diff from parent to child while rewriting in
* order to have a smaller final write. */
if (flags & RDB_SAVE_AOF_PREAMBLE &&
rdb->processed_bytes > processed+AOF_READ_DIFF_INTERVAL_BYTES)
{
processed = rdb->processed_bytes;
aofReadDiffFromParent();
}
}
dictReleaseIterator(di);
...
}
...
通过上面的代码我们可以看到,rdbSaveRio
函数会先将 SELECTDB
操作码和对应的数据库编号写⼊ RDB
⽂件中,这样方便解析时知道下面的数据是哪个数据库的。然后 rdbSaveRio
函数会写⼊ RESIZEDB
操作码,⽤来标识全局哈希表和过期 key
哈希表中键值对数量。最后 rdbSaveRio
函数会遍历当前数据库的所有键值对,把键、值以及过期时间写入 RDB
文件中。到这文件数据就写入完成了。
文件尾
写入文件尾的操作比较简单,主要写入两部分内容,一个是文件结束的操作码标识,另一个是校验码。下面我们看一下代码中是如何实现的:
...
// 写入结束操作码
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. */
cksum = rdb->cksum;
memrev64ifbe(&cksum);
// 写入校验码
if (rioWrite(rdb,&cksum,8) == 0) goto werr;
...
好了,到这我们就分析完 Redis
中 RDB
文件的创建过程。
小结
本文主要介绍了内存快照文件 RDB
的三个入口函数以及创建 RDB
文件的时机还有 RDB
文件的生成过程。RDB
文件保存了 Redis
某一时刻所有的键值对,以及这些键值对的类型、大小、过期时间等信息。
转载自:https://juejin.cn/post/7173930415558754341