咱们小学三年级的时候就晓得,redis是一个纯内存存储的中间件,那它宕机会怎么样?数据会失落吗?答案是能够不丢。 事实上redis为了保障宕机时数据不失落,提供了两种数据长久化的机制——rdb和aof。

rdb就定期将内存里的数据全量dump到磁盘里,下次启动时就能够间接加载之前的数据了,rdb的问题是它只能提供某个时刻的数据快照,无奈保障建设快照后的数据不丢,所以redis还提供了aof。aof全程是Append Only File,它的原理就是把所有改变一条条写到磁盘上。这篇博客咱们来重点介绍下rdb持久性的实现,aof留到下一篇博客。

rdb相干源码

在redis中,触发rdb保留次要有以下几种形式。

save命令

咱们在redis-cli下间接调用save命令就会触发rdb文件的生成,如果后盾没有在子过程在生成rdb,就会调用rdbSave()生成rdb文件,并将其保留在磁盘中。

void saveCommand(client *c) {    // 查看是否后盾曾经有过程在执行save,如果有就进行执行。    if (server.child_type == CHILD_TYPE_RDB) {        addReplyError(c,"Background save already in progress");        return;    }    rdbSaveInfo rsi, *rsiptr;    rsiptr = rdbPopulateSaveInfo(&rsi);    if (rdbSave(server.rdb_filename,rsiptr) == C_OK) {        addReply(c,shared.ok);    } else {        addReplyErrorObject(c,shared.err);    }}

Redis 的 rdbSave 函数是真正进行 RDB 长久化的函数,它的大抵流程如下:

  1. 首先创立一个临时文件。
  2. 创立并初始化rio,rio是redis对io的一种形象,提供了read、write、flush、checksum……等办法。
  3. 调用 rdbSaveRio(),将以后 Redis 的内存信息全量写入到临时文件中。
  4. 调用 fflush、 fsync 和 fclose 接口将文件写入磁盘中。
  5. 应用 rename 将临时文件改名为 正式的 RDB 文件。
  6. 将server.dirty清零,server.dirty是用了记录在上次生成rdb后有多少次数据变更,会在serverCron中用到。

具体代码如下:

/* rdb磁盘写入操作 */int rdbSave(char *filename, rdbSaveInfo *rsi) {    char tmpfile[256];    char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */    FILE *fp = NULL;    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;    }    rioInitWithFile(&rdb,fp);  // 初始化rio,    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;    }    /* 把数据刷到磁盘删,确保操作系统缓冲区没有残余数据 */    if (fflush(fp)) goto werr;    if (fsync(fileno(fp))) goto werr;    if (fclose(fp)) { fp = NULL; goto werr; }    fp = NULL;        /* 把临时文件重命名为正式文件名 */    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));    if (fp) fclose(fp);    unlink(tmpfile);    stopSaving(0);    return C_ERR;}

因为redis是单线程模型,所以在save的过程中解决不了申请,单线程模型能够save的过程中不会有数据变动,但save可能会继续很久,这会导致redis无奈失常解决读写申请,对于线上服务来说这是十分致命的,所以redis还提供了bgsave命令,它能够在不影响失常读写的状况下执行save操作,咱们来看下具体实现。

bgsave命令

bgsave提供了后盾生成rdb文件的性能,bg含意就是background,具体怎么实现的? 其实就是调用fork() 生成了一个子过程,而后在子过程中实现了save的过程。

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 {            addReplyErrorObject(c,shared.syntaxerr);            return;        }    }    rdbSaveInfo rsi, *rsiptr;    rsiptr = rdbPopulateSaveInfo(&rsi);    if (server.child_type == CHILD_TYPE_RDB) {        addReplyError(c,"Background save already in progress");    } else if (hasActiveChildProcess()) {        if (schedule) {            server.rdb_bgsave_scheduled = 1;  // 如果bgsave曾经在执行中了,这次执行会放到serverCron中执行            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.");        }    } else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) {        addReplyStatus(c,"Background saving started");    } else {        addReplyErrorObject(c,shared.err);    }}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);    // 创立子过程,redisFork理论就是对fork的封装     if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) {        int retval;        /* 子过程 */        redisSetProcTitle("redis-rdb-bgsave");        redisSetCpuAffinity(server.bgsave_cpulist);        retval = rdbSave(filename,rsi);        if (retval == C_OK) {            sendChildCowInfo(CHILD_INFO_TYPE_RDB_COW_SIZE, "RDB");        }        exitFromChild((retval == C_OK) ? 0 : 1);    } else {        /* 父过程 */        if (childpid == -1) {            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 %ld",(long) childpid);        server.rdb_save_time_start = time(NULL);        server.rdb_child_type = RDB_CHILD_TYPE_DISK;        return C_OK;    }    return C_OK; /* unreached */}

bgsave其实就是把save的流程放到子过程里执行,这样就不会阻塞到父过程了。我最开始看到这里的时候有个问题,父过程在继续读写内存的状况下子过程是如何保留某一时刻快照的? 这个redid中没有非凡解决,还是依赖了操作系统提供的fork()。
当一个过程调用fork()时,操作系统会复制一份以后的过程,包含以后过程中的内存内容。所以能够认为只有fork()胜利,以后内存中的数据就被全量复制了一份。当然具体实现上内核为了晋升fork()的性能,应用了copy-on-write的技术,只有被复制的数据在被父过程或者子过程改变时才会真正拷贝。

serverCron

下面生成rdb的两种形式都是被动触发的,redis也提供定期生成rdb的机制。redis对于rdb生成的配置如下:

save <seconds> <changes> ## 例如save 3600 1 # 3600秒内如果有1条写书就生成rdbsave 300 100 # 300秒内如果有100条写书就生成rdbsave 60 10000 # 60秒内如果有1000条写书就生成rdb

定期生成rdb的实现在server.c 中的serverCron中。serverCron是redis每次执行完一次eventloop执行的定期调度工作,外面就有rdb和aof的执行逻辑,rdb相干具体如下:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {    /*    . 略去其余代码    */    /* 检测bgsave和aof重写是否在执行过程中 */    if (hasActiveChildProcess() || ldbPendingChildren())    {        run_with_period(1000) receiveChildInfo();        checkChildrenDone();    } else {        /* If there is not a background saving/rewrite in progress check if         * we have to save/rewrite now. */        for (j = 0; j < server.saveparamslen; j++) {            struct saveparam *sp = server.saveparams+j;            /* 查看是否达到了执行save的规范 */            if (server.dirty >= sp->changes &&                server.unixtime-server.lastsave > sp->seconds &&                (server.unixtime-server.lastbgsave_try >                 CONFIG_BGSAVE_RETRY_DELAY ||                 server.lastbgsave_status == C_OK))            {                serverLog(LL_NOTICE,"%d changes in %d seconds. Saving...",                    sp->changes, (int)sp->seconds);                rdbSaveInfo rsi, *rsiptr;                rsiptr = rdbPopulateSaveInfo(&rsi);                rdbSaveBackground(server.rdb_filename,rsiptr);                break;            }        }    }    /*    . 略去其余代码    */    /* 如果上次触发bgsave时曾经有过程在执行了,就会标记rdb_bgsave_scheduled=1,而后放到serverCron     * 中执行      */    if (!hasActiveChildProcess() &&        server.rdb_bgsave_scheduled &&        (server.unixtime-server.lastbgsave_try > CONFIG_BGSAVE_RETRY_DELAY ||         server.lastbgsave_status == C_OK))    {        rdbSaveInfo rsi, *rsiptr;        rsiptr = rdbPopulateSaveInfo(&rsi);        if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK)            server.rdb_bgsave_scheduled = 0;    }    /*    . 略去其余代码    */}

rdb文件格式

rdb的具体文件格式绝对比较简单,具体如下:

----------------------------#52 45 44 49 53              # 魔术 "REDIS"30 30 30 33                 # ASCII码rdb的版本号 "0003" = 3----------------------------FA                          # 辅助字段$string-encoded-key         # 可能蕴含多个元信息 $string-encoded-value       # 比方redis版本号,创立工夫,内存使用量……...----------------------------FE 00                       # redis db号. db number = 00FB                          # 标识db的大小$length-encoded-int         # hash表的大小(int)$length-encoded-int         # expire hash表的大小(int)----------------------------# 从这里开始就是具体的k-v数据 FD $unsigned-int            # 数据还有多少秒过期(4byte unsigned int)$value-type                 # 标识value数据类型(1 byte)$string-encoded-key         # key,redis字符串类型(sds)$encoded-value              # value, 类型取决于 $value-type----------------------------FC $unsigned long           # 数据还有多少毫秒过期(8byte unsigned long)$value-type                 # 标识value数据类型(1 byte)$string-encoded-key         # key,redis字符串类型(sds) $encoded-value              # value, 类型取决于 $value-type----------------------------$value-type                 # redis数据key-value,没有过期工夫$string-encoded-key$encoded-value----------------------------FE $length-encoding         # FE标识前一个db的数据完结,而后再加上数据的长度 ----------------------------...                         # 其余redis db中的k-v数据, ...FF                          # FF rdb文件的完结标识  8-byte-checksum             ## 最初是8byte的CRC64校验和 

总结

rdb在肯定水平上保障了redis实例在异样宕机时数据不丢,当因为是定期生成的rdb快照,在生成快照后产生的变动无奈追加到rdb文件中,所以rdb无奈彻底保证数据不丢,为此redis又提供了另外一种数据长久化机制aof,咱们将在下篇文章中看到。另外,在执行bgsave的时候高度依赖于操作系统的fork()机制,这也是会带来很大的性能开销的,详见Linux fork暗藏的开销-过期的fork(正传)

参考资料

  1. Redis RDB 长久化详解
  2. https://rdb.fnordig.de/file_format.html
  3. Redis Persistence
  4. Linux fork暗藏的开销-过期的fork(正传)
本文是Redis源码分析系列博文,同时也有与之对应的Redis中文正文版,有想深刻学习Redis的同学,欢送star和关注。
Redis中文注解版仓库:https://github.com/xindoo/Redis
Redis源码分析专栏:https://zxs.io/s/1h
如果感觉本文对你有用,欢送一键三连