redis rdb机制浅析 作者: nbboy 时间: 2020-08-24 分类: 软件架构,软件工程,设计模式 评论 ###dump rdb文件的流程: 1.redis fork()调用,创建子进程 2.利用copy-on-write技术,对内存dump出来到一个临时文件 3.完成dump文件后,替换该临时文件为dump.rdb文件 ###需要注意的几个问题: 1.dump的是执行这条命令时候的数据 2.从上面步骤可以看到,任何时候dump.rdb文件其实都是完整的 3.可以执行save,bgsave来手动执行dump操作,或者配置save规则让redis自动执行,其实也是执行bgsave 4.redis重启后,读取dump.rdb文件开始从磁盘到内存的数据加载过程,这个过程是阻塞的,直到完成加载 5.rdb方式会丢失数据,这部分数据就是从上一次dump到redis挂掉为止修改的数据 通过调试跟踪,bgsave命令最终的实现的文件在rdb.c里的bgsaveCommand ```c /* BGSAVE [SCHEDULE] bgsave执行的命令 */ void bgsaveCommand(client *c) { int schedule = 0; /* 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) { if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) { schedule = 1; } else { addReply(c,shared.syntaxerr); return; } } rdbSaveInfo rsi, *rsiptr; //初始化rdb集群相关信息 rsiptr = rdbPopulateSaveInfo(&rsi); //如果bgsave进程还在执行中,则返回错误 if (server.rdb_child_pid != -1) { addReplyError(c,"Background save already in progress"); //这里也一样,如果系统还在执行gsave进程,或者执行aof重写进程,或者执行模块进程则选择不执行 } else if (hasActiveChildProcess()) { if (schedule) { server.rdb_bgsave_scheduled = 1; addReplyStatus(c,"Background saving scheduled"); } else { addReplyError(c, "Another child process is active (AOF?): can't BGSAVE right now. " "Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever " "possible."); } //执行真正的bgsave } else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) { addReplyStatus(c,"Background saving started"); } else { addReply(c,shared.err); } } ``` 通过上面的代码,我们可以看到,aof重写进程在工作的时候,bgsave不会真正工作!在rdbSaveBackground里看下做了什么事情? ```c int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) { pid_t childpid; //再一次检测工作进程执行情况 if (hasActiveChildProcess()) return C_ERR; server.dirty_before_bgsave = server.dirty; server.lastbgsave_try = time(NULL); //建立父子进程通信之间的管道 openChildInfoPipe(); //执行包装的fork if ((childpid = redisFork()) == 0) { int retval; /* Child */ redisSetProcTitle("redis-rdb-bgsave"); redisSetCpuAffinity(server.bgsave_cpulist); //执行真正的保存rdb文件逻辑 retval = rdbSave(filename,rsi); if (retval == C_OK) { //用建立的管道,通知父进程,子进程已经结束的通知 sendChildCOWInfo(CHILD_INFO_TYPE_RDB, "RDB"); } exitFromChild((retval == C_OK) ? 0 : 1); } else { /* Parent */ if (childpid == -1) { closeChildInfoPipe(); server.lastbgsave_status = C_ERR; serverLog(LL_WARNING,"Can't save in background: fork: %s", strerror(errno)); return C_ERR; } serverLog(LL_NOTICE,"Background saving started by pid %d",childpid); server.rdb_save_time_start = time(NULL); server.rdb_child_pid = childpid; server.rdb_child_type = RDB_CHILD_TYPE_DISK; return C_OK; } return C_OK; /* unreached */ } ``` 代码注释非常详细了,可以看到用fork创建了子进程,真正的dump工作都是在子进程中完成的。因为操作系统实现的copy-on-write机制,所以fork后其实子进程地址空间和父进程地址空间还是同一个,所以数据完全可以dump出来,关于copy-on-write可以看下,这篇文章的理论知识https://wingsxdu.com/post/linux/concurrency-oriented-programming/fork-and-cow/ 进一步阅读rdbSave函数,具体的dump工作就是在这个函数里进行的。 ```c /* Save the DB on disk. Return C_ERR on error, C_OK on success. */ int rdbSave(char *filename, rdbSaveInfo *rsi) { char tmpfile[256]; char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */ FILE *fp; rio rdb; int error = 0; //建立临时文件,后续的rdb内容都先写到这个临时文件中 snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid()); fp = fopen(tmpfile,"w"); if (!fp) { char *cwdp = getcwd(cwd,MAXPATHLEN); 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是作者抽象的流式文件对象 rioInitWithFile(&rdb,fp); //通知模块触发持久化的事件 startSaving(RDBFLAGS_NONE); if (server.rdb_save_incremental_fsync) rioSetAutoSync(&rdb,REDIS_AUTOSYNC_BYTES); //这里执行真正的dump逻辑,涉及到很多rdb文件格式细节 if (rdbSaveRio(&rdb,&error,RDBFLAGS_NONE,rsi) == C_ERR) { errno = error; goto werr; } //下面的几个操作,都是保证让buffer中的数据都写入到磁盘 //关于这方面的讨论具体可以看下作者的讨论:http://oldblog.antirez.com/post/redis-persistence-demystified.html /* 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; /* Use RENAME to make sure the DB file is changed atomically only * if the generate DB file is ok. */ //把临时文件重新命名为dump.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); stopSaving(0); return C_ERR; } serverLog(LL_NOTICE,"DB saved on disk"); server.dirty = 0; server.lastsave = time(NULL); server.lastbgsave_status = C_OK; //通知模块持久化结束的事件 stopSaving(1); return C_OK; werr: serverLog(LL_WARNING,"Write error saving DB on disk: %s", strerror(errno)); fclose(fp); //执行错误的话,需要删除临时文件 unlink(tmpfile); stopSaving(0); return C_ERR; } ``` ###从上面代码代码可以得出结论: 1)都是先写到临时文件temp-dump.rdb中,然后再重命名为正式文件dump.rdb 2) 在持久化之前和之后,模块会收到开始和结束事件 3)rdbSaveRio根据rdb特有格式写到文件中,其实rdb也用在集群复制中 4)写到文件后,数据都会从用户缓存区和内核缓冲区强制写到磁盘驱动中去,也就是实现落盘,这样即使redis服务崩溃,或者系统崩溃这两个级别错误,都可以应对。 ###总结: rdbSaveRio中是具体的dump逻辑,根据rdb格式dump出来,这个文件格式的具体描述可以看https://github.com/sripathikrishnan/redis-rdb-tools/wiki/Redis-RDB-Dump-File-Format ,因为这个逻辑不是本篇文章的重点,所以不再叙述。 参考: https://blog.csdn.net/aitangyong/article/details/52045251