Redis RDB 持久化源码深度解析:从原理到实现

为避免服务器宕机着情况导致redis内存数据库数据丢失,redis默认出通过rdb保证可靠性,本文将从源码的角度带读者了解rdb读写时机和写入流程。

save指令触发rdb

redis支持通过命令的方式持久化内存数据库数据,当我们键入save的时候,redis解析到这个指令之后,主线程直接调用saveCommand方法生成rdb文件落到磁盘中。

我们可以在rdb.c文件中看到该方法的实现,可以看到为了避免脏写等问题,saveCommand会检查当前是否有rdb子进程执行,如果没有在子进程执行rdb持久化则直接调用rdbSave方法生成dump.rdb文件落盘:

复制
//调用save指令其内部调用rdbSave完成rdb文件生成 void saveCommand(redisClient *c) { //检查是否子进程执行rdb,若有则直接返回 if (server.rdb_child_pid != -1) { addReplyError(c,"Background save already in progress"); return; } //调用rdbSave if (rdbSave(server.rdb_filename) == REDIS_OK) { addReply(c,shared.ok); } else { addReply(c,shared.err); } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.

步入rdbSave即可看到生成临时rdb写入数据,然后数据刷盘,最后完成文件名原子修改的操作:

复制
int rdbSave(char *filename) { char tmpfile[256]; FILE *fp; rio rdb; int error; //生成一个tmp文件 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; } //调用rdbSaveRio完成数据写入 rioInitWithFile(&rdb,fp); if (rdbSaveRio(&rdb,&error) == REDIS_ERR) { errno = error; goto werr; } //直接刷盘到磁盘,避免留在系统输出缓冲区 /* Make sure data will not remain on the OSs output buffers */ if (fflush(fp) == EOF) goto werr; if (fsync(fileno(fp)) == -1) goto werr; if (fclose(fp) == EOF) goto werr; //完成写入后文件重命名为dump.rdb 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; } //...... return REDIS_OK; //...... }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.
bgsave指令触发rdb

同时redis也支持后台持久化,如果用户需要考虑redis性能问题,可以直接通过bgsave指令创建rdb子进程完成数据库数据持久化。

我们同样可以在rdb.c文件中看到bgsave指令调用的方法bgsaveCommand,可以看到如果没有子进程进行rdb或者aof,该指令会调用rdbSaveBackground完成异步数据持久化:

复制
//调用rdbSaveBackground创建一个子进程生成rdb文件,不影响主线程 void bgsaveCommand(redisClient *c) { //如果有子进程执行rdb或者aof,则直接返回错误提醒 if (server.rdb_child_pid != -1) { addReplyError(c,"Background save already in progress"); } else if (server.aof_child_pid != -1) { addReplyError(c,"Cant BGSAVE while AOF log rewriting is in progress"); } else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {//调用rdbSaveBackground进行数据持久化 addReplyStatus(c,"Background saving started"); } else { addReply(c,shared.err); } }1.2.3.4.5.6.7.8.9.10.11.12.13.

步入rdbSaveBackground可以看到,其内部还会检查一次是否有文件进行rdb,如果明确没有之后直接fork一个子进程出来调用上文所说的rdbSave完成数据持久化到dump.rdb中:

复制
int rdbSaveBackground(char *filename) { pid_t childpid; long long start; if (server.rdb_child_pid != -1) return REDIS_ERR; //...... start = ustime(); if ((childpid = fork()) == 0) {//创建子进程 int retval; //...... retval = rdbSave(filename);//生成rdb文件 exitFromChild((retval == REDIS_OK) ? 0 : 1);//退出子进程 } else { //...... } return REDIS_OK; /* unreached */ }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.
rdb被动触发

redis被动触发由时间事件轮询处理,我们可以在redis.conf配置rdb被动触发持久化的时机,默认配置如下当60s生成10000或者300s 生成10次改变亦或者900s生成1次改变,我们就会执行一次被动rdb持久化:

复制
save 900 1 save 300 10 save 60 100001.2.3.

对应的我们可以在redis.c的serverCron函数在看到这段逻辑,它会遍历出我们配置的保存间隔配置saveparam,通过比对这3条配置的上次保存时间计算出时间间隔,以及当前redis变化书dirty看看是否符合要求,若如何要求则进行后台rdb持久化:

复制
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { //...... /* Check if a background saving or AOF rewrite in progress terminated. */ if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) { //...... } } else { //遍历3个配置的params,如果改变数和事件间隔配置要求则直接进行后台被动rdb持久化 for (j = 0; j < server.saveparamslen; j++) { struct saveparam *sp = server.saveparams+j; if (server.dirty >= sp->changes && //查看变化数是否大于当前配置的changes server.unixtime-server.lastsave > sp->seconds && //查看时间间隔是否大于配置 (server.unixtime-server.lastbgsave_try > REDIS_BGSAVE_RETRY_DELAY || server.lastbgsave_status == REDIS_OK)) { //...... //执行异步持久化 rdbSaveBackground(server.rdb_filename); break; } } //...... } } //...... return 1000/server.hz; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.
其他被动落盘时机

其实有些时候我们执行的某些执行也会进行rdb持久化,例如flushall刷盘指令,其调用函数flushallCommand就会时间串行执行rdb持久化:

复制
//调用flush指令时会调用rdbSave进行数据持久化 void flushallCommand(redisClient *c) { //...... if (server.saveparamslen > 0) { //串行执行rdb持久化 int saved_dirty = server.dirty; rdbSave(server.rdb_filename); //...... } server.dirty++; }1.2.3.4.5.6.7.8.9.10.11.

当我们关闭redis服务器的时候也会执行rdb串行持久化:

复制
//服务器进程关闭时调用rdbSave生成rdb文件 int prepareForShutdown(int flags) { //...... if (server.rdb_child_pid != -1) { //...... } if (server.aof_state != REDIS_AOF_OFF) { //...... } if ((server.saveparamslen > 0 && !nosave) || save) { if (rdbSave(server.rdb_filename) != REDIS_OK) { //...... return REDIS_ERR; } } //...... return REDIS_OK; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.
rdb写入文件数据详解

无论是rdbsave还是rdbbgsave对应的方法,其内部都会调用rdbSaveRio,它进行文件写入时对应写入数据大体顺序是:

写入REDIS大写。补0填充长度。写入当前redis版本号,以笔者源码为例则是6。遍历数据库写入REDIS_RDB_OPCODE_SELECTDB表示开始存储数据库数据,这个值默认为254,redis会转为八进制376写入。遍历当前数据库键值对key长度和key,value长度和value写入,后续数据库都是如此往复。所有数据库写完后补上REDIS_RDB_OPCODE_EOF和checksum用于后续rdb数据恢复的校验。

为保证读者更直观的了解redis持久化写入的内容,我们可以删除本地rdb文件,然后执行如下执行生成一个全新的rdb文件:

复制
# 保存键值对 set key value # 切换到1库 select 1 # 保存键值对到1set key-1 value # 调用save进行数据持久化 save1.2.3.4.5.6.7.8.

正常情况下我们打开rdb文件会得到一堆类型乱码的内容,我们无法知晓写入的信息,我们可以直接键入od生成rdb文件16进制数据及其对应的ASCII字符:

复制
od -A x -t x1c -v dump.rdb1.

最终我们就可以得到如下文件,可以看到数据格式和笔者上文所说基本一致:

复制
# 大写REDIS0 2548进制 当前数据库索引 键值对`key`长度和`key``value`长度和`value` #000000 52 45 44 49 53 30 30 30 36 fe 00 00 03 6b 65 79 R E D I S 0 0 0 6 376 \0 \0 003 k e y 000010 05 76 61 6c 75 65 fe 01 00 05 6b 65 79 2d 31 05 005 v a l u e # 2548进制 当前数据库索引1 键值对key长度和key,value长度和value 376 001 \0 005 k e y - 1 005 000020 76 61 6c 75 65 ff 76 eb e4 80 bd df 66 11 v a l u e # EOF 255八进制 剩下8位是对应的checksum 377 v 353 344 200 275 337 f 021 00002e1.2.3.4.5.6.7.8.9.10.11.12.

对应的我们给出这段源码,对应的写入流程如上文笔者所述:

复制
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; snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);//对应redis 3个0 然后版本号,当前版本为6 if (rdbWriteRaw(rdb,magic,9) == -1) goto werr;//上述魔数写入rdb文件 //遍历数据库 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 REDIS_ERR; /* Write the SELECT DB opcode */ if (rdbSaveType(rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;//写入254,也就是内容中的376 if (rdbSaveLen(rdb,j) == -1) goto werr;//写入当前库索引 //遍历当前键值对写入 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); } //...... /* EOF opcode */ if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;//写入结束符254 八进制为377 cksum = rdb->cksum; memrev64ifbe(&cksum); if (rioWrite(rdb,&cksum,8) == 0) goto werr;//写入8位数校验和,其底层调用rioGenericUpdateChecksum,按照cksum到数组中获取就对应的值并 return REDIS_OK; //...... }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.

对应的我们步入rdbSaveKeyValuePair即可看到redis获取key长度和key,以及value长度和value并写入rdb文件的核心流程:

复制
int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val, long long expiretime, long long now) { //...... /* Save type, key, value */ if (rdbSaveObjectType(rdb,val) == -1) return -1;//写入类型以字符串形式就是0 if (rdbSaveStringObject(rdb,key) == -1) return -1;//写入key长度和key if (rdbSaveObject(rdb,val) == -1) return -1;//写入value长度和value return 1; }1.2.3.4.5.6.7.8.9.10.11.

阅读剩余
THE END