关于java:Redis源码剖析之RDB

5次阅读

共计 7435 个字符,预计需要花费 19 分钟才能阅读完成。

咱们小学三年级的时候就晓得,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 条写书就生成 rdb
save 300 100 # 300 秒内如果有 100 条写书就生成 rdb
save 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 = 00
FB                          # 标识 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
如果感觉本文对你有用,欢送 一键三连

正文完
 0