Redis数据持久化之RDB方式

机会只留给那些准备充分的人

Posted by irving.gx on April 17, 2020

Redis是一种内存数据库,它将自己的数据保存在内存当中,这也是访问redis之所以快速的一个原因。将数据放在内存当中的一个问题就是当服务器进程退出 的时候数据会丢失,那么为了在一段时间内能够长久保存这些数据就需要将这些数据持久化,也就是保存在硬盘当中,redis提供了两种方式来进行数据的持久化, RDB以及AOF这两种持久化方式。在这一章当中我们先分析一下RDB方式。

RDB文件是一种二进制格式的压缩文件,redis在内存当中存储的数据会以RDB文件的形式持久化到硬盘当中,这是一种全量数据持久化的方式,通过这个文件 可以还原出redis当中的数据。有两个命令可以主动触发生成生成RDB文件,一个是SAVE,一个是BGSAVE,这两者的区别是前者是会阻塞主进程,服务器在此 期间不会处理其他的命令请求。而后者是fork出一个子进程,由子进程来生成RDB文件,并不会阻塞当前的进程。我们看一下源码这两个命令的实现逻辑分别 是什么,save以及bgsave命令都在src/rdb.c这个文件当中

我们看到SAVE命令的逻辑是比较简单的,首先在2390行判断一下后台是否已经有了子进程在执行持久化过程,如果有这样的子进程,那再执行一次save就没有 什么意义了。如果没有,那么我们可以看到在这个主进程中进行rdbSave,在2395行,通过执行rdbPopulateSaveInfo函数生成rdb的保存信息,然后调 用rdbSave方法来生成rdb文件。具体rdbSave是什么逻辑我们后面细说,我们再来看一下另外一个命令BGSAVE命令

/* BGSAVE [SCHEDULE] */
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;
    rsiptr = rdbPopulateSaveInfo(&rsi);
//是否有子进程在进行持久化
    if (server.rdb_child_pid != -1) {
        addReplyError(c,"Background save already in progress");
        //aof持久化模式是否正在进行
    } else if (server.aof_child_pid != -1) {
        //如果aof在进行的话,当前bgsave是否进入了调度
        if (schedule) {
            server.rdb_bgsave_scheduled = 1;
            addReplyStatus(c,"Background saving scheduled");
        } else {
            addReplyError(c,
                "An AOF log rewriting in progress: can't BGSAVE right now. "
                "Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever "
                "possible.");
        }
        //在后台执行生成rdb文件
    } else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) {
        addReplyStatus(c,"Background saving started");
    } else {
        addReply(c,shared.err);
    }
}

对于一些比较核心的部分,我用中文做了注释,结合源代码注释以及加上的注释我们可以看到在生成rdb文件之前会先判断是否有rdb子进程已经在执行持久化 ,如果没有就判断一下是否有aof模式正在进行,如果有的话看当前进程是否处于调度中,如果以上的前提都没有那表示后台没有持久化的进程,然后才会调 用rdbSaveBackground方法来进行持久化,我们看一下rdbSaveBackground这个方法的代码

int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
    pid_t childpid;
    long long start;
    //检查后台是否正在执行rdb以及aof操作
    if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
    //dirty用来存储上次保存前数据变动的次数,如果对于数据有变动,那么这个字段会被累加
    //源代码中对于dirty的注释是这样的 Changes to DB from the last save
    server.dirty_before_bgsave = server.dirty;
    //bgsave时间
    server.lastbgsave_try = time(NULL);
    //打开子进程管道
    openChildInfoPipe();
start = ustime();
    //生成子进程
    if ((childpid = fork()) == 0) {
        int retval;
        //子进程
        /* Child */
        //关闭子进程的socket的监听
        closeListeningSockets(0);
        redisSetProcTitle("redis-rdb-bgsave");
        //进行子进程写入操作
        retval = rdbSave(filename,rsi);
        if (retval == C_OK) {
            size_t private_dirty = zmalloc_get_private_dirty(-1);
if (private_dirty) {
                serverLog(LL_NOTICE,
                    "RDB: %zu MB of memory used by copy-on-write",
                    private_dirty/(1024*1024));
            }
server.child_info_data.cow_size = private_dirty;
            sendChildInfo(CHILD_INFO_TYPE_RDB);
        }
        //退出子进程
        exitFromChild((retval == C_OK) ? 0 : 1);
    } else {
        /* Parent */
        //父进程
        //fork统计时间
        server.stat_fork_time = ustime()-start;
        server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
        latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
        //如果子进程创建失败
        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;
        updateDictResizePolicy();
        return C_OK;
    }
    return C_OK; /* unreached */
}

我们可以看到对于BGSAVE命令是在后台开启了一个子进程来执行rdb文件的生成操作。在子进程中仍然是调用了rdbSave函数,和SAVE命令调用的函数一样。 我们看一下rdbSave函数的逻辑是什么


/* 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;
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;
    }
    //初始化IO
    rioInitWithFile(&rdb,fp);
if (server.rdb_save_incremental_fsync)
        rioSetAutoSync(&rdb,REDIS_AUTOSYNC_BYTES);
    //写入操作主要逻辑
    if (rdbSaveRio(&rdb,&error,RDB_SAVE_NONE,rsi) == C_ERR) {
        errno = error;
        goto werr;
    }
/* 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. */
     //重命名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);
        return C_ERR;
    }
serverLog(LL_NOTICE,"DB saved on disk");
    server.dirty = 0;
    server.lastsave = time(NULL);
    server.lastbgsave_status = C_OK;
    return C_OK;
werr:
    //日志报错
    serverLog(LL_WARNING,"Write error saving DB on disk: %s", strerror(errno));
    fclose(fp);
    unlink(tmpfile);
    return C_ERR;
}

对于一些主要的逻辑在代码中加上了注释,以上就是RDB方式的一些解读。在下一篇我们分析一下AOF方式。


如果对你有帮助,请作者喝一杯牛奶吧