阅读 249

RDB工作流程解析

这里只介绍rdb是在什么时候触发的,rdb本身的数据格式不会被介绍,通过观察代码大体可以发现,就是按照约定的格式对redisObject进行加工,或者按照解析将数据流还原成redisObject。有关rdb的加载逻辑在redis的初始化流程中已经有介绍了。现在就来看看什么时候会触发rdb的生成。

首先在有关aof的解析中可以看到。对应瘦身aof文件的生成,根据server的配置项会选择先生成rdb数据。

对应的代码为

int rewriteAppendOnlyFile(char *filename) {
    ...
    // 代表该aof文件还存储了rdb格式的数据  这种aof文件恢复数据会更快
    if (server.aof_use_rdb_preamble) {
        int error;
        // 先将快照数据存储到rdb文件中   当作为aof生成rdb数据时  rdbflags是 RDBFLAGS_AOF_PREAMBLE 这样在生成rdb数据的同时 还会抽空从父进程中读取后写入的aof数据
        if (rdbSaveRio(&aof,&error,RDBFLAGS_AOF_PREAMBLE,NULL) == C_ERR) {
            errno = error;
            goto werr;
        }
    } else {
        // 基于当前db的数据 直接产生最简的command 相当于瘦身后的aof
        if (rewriteAppendOnlyFileRio(&aof) == C_ERR) goto werr;
    }
复制代码

之后再来看看还有哪些动作会触发rdb文件的生成,可以发现redis对外暴露2个command

saveCommand:

/**
 * 执行save命令 就是基于当前数据生成快照(rdb)
 * @param c
 */
void saveCommand(client *c) {
    if (server.rdb_child_pid != -1) {
        addReplyError(c,"Background save already in progress");
        return;
    }
    rdbSaveInfo rsi, *rsiptr;
    // 根据当前服务器信息填充 rdbSaveInfo
    rsiptr = rdbPopulateSaveInfo(&rsi);
    // 根据保存结果返回信息到client
    if (rdbSave(server.rdb_filename,rsiptr) == C_OK) {
        addReply(c,shared.ok);
    } else {
        addReply(c,shared.err);
    }
}
复制代码

rdbSave()这个方法就不详细展开了,就是挨个处理每个db下的redisObject对象,将它们按照rdb协议规定的格式写入到文件中,在转换过程中往往会涉及到压缩算法或者一些编码技巧,用于节省内存,这个套路跟lucene一致。 除此之外在准备关闭redis服务器时也会尝试生成rdb文件。

/**
 * 准备关闭redis服务器
 * @param flags
 * @return
 */
int prepareForShutdown(int flags) {
...
/* Create a new RDB file before exiting.
     * 如果强制要求了save 就会生成一份rdb数据
     * 如果没有强制要求save 就会检测此时是否满足saveparam条件
     * 如果要求了nosave 就必然不会生成rdb数据
     * */
    if ((server.saveparamslen > 0 && !nosave) || save) {
        serverLog(LL_NOTICE,"Saving the final RDB snapshot before exiting.");
        // 打印一些监督信息 先忽略
        if (server.supervised_mode == SUPERVISED_SYSTEMD)
            redisCommunicateSystemd("STATUS=Saving the final RDB snapshot\n");
        /* Snapshotting. Perform a SYNC SAVE and exit */
        rdbSaveInfo rsi, *rsiptr;
        // 从server上获取一些信息 并填充到rdbSaveInfo中
        rsiptr = rdbPopulateSaveInfo(&rsi);
        // 生成快照数据 并保存到rdb文件中
        if (rdbSave(server.rdb_filename,rsiptr) != C_OK) {
            /* Ooops.. error saving! The best we can do is to continue
             * operating. Note that if there was a background saving process,
             * in the next cron() Redis will be notified that the background
             * saving aborted, handling special stuff like slaves pending for
             * synchronization... */
复制代码

之后还有使用子进程生成rdb数据的api--bgsaveCommand

/* BGSAVE [SCHEDULE]
 * 使用子进程执行rdb生成任务
 * */
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");
        // 此时有其他子进程正在运行 如果本次允许延迟处理 返回延迟处理的信息 否则返回失败的信息
    } 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.");
        }
        // 使用后台线程生成rdb
    } else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) {
        addReplyStatus(c,"Background saving started");
    } else {
        addReply(c,shared.err);
    }
}
复制代码

server.rdb_bgsave_scheduled这个标记的作用和那个延迟执行aof的标记作用一致,也是在serverCron的主循环中发现该标记,并开启子进程生成rdb。

然后在serverCron中还会根据saveparam参数尝试执行rdb操作

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
...
/* If there is not a background saving/rewrite in progress check if
         * we have to save/rewrite now.
         * 此时没有正在执行的后台任务,判断是否满足saveparam条件 并触发存储rdb逻辑
         * */
        for (j = 0; j < server.saveparamslen; j++) {
            // 遍历所有保存条件
            struct saveparam *sp = server.saveparams+j;

            /* Save if we reached the given amount of changes,
             * the given amount of seconds, and if the latest bgsave was
             * successful or if, in case of an error, at least
             * CONFIG_BGSAVE_RETRY_DELAY seconds already elapsed.
             * 只要满足任意一个保存条件 即触发rdb生成
             * */
            if (server.dirty >= sp->changes &&
                server.unixtime-server.lastsave > sp->seconds &&
                // 如果上一次生成rdb是成功的 那么本次也可以继续尝试生成rdb 如果上一次生成rdb是失败的 那么会有一个等待时间 必须间隔这么长时间后才能生成rdb
                (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;
                // 通过db信息填充saveInfo
                rsiptr = rdbPopulateSaveInfo(&rsi);
                rdbSaveBackground(server.rdb_filename,rsiptr);
                break;
            }
        }
复制代码

而rdbSaveBackground的核心逻辑就是使用子进程执行rdbSave,这样就和前面关联起来了。 最后当子进程完成任务时,根据触发rdb的任务类型执行不同的后置函数

/* When a background RDB saving/transfer terminates, call the right handler.
 * rdb子进程被完全回收时执行该方法
 * */
void backgroundSaveDoneHandler(int exitcode, int bysignal) {
    int type = server.rdb_child_type;
    switch(server.rdb_child_type) {
    case RDB_CHILD_TYPE_DISK:
        // 当负责将rdb数据写入到文件的子进程完成工作后 触发该方法 判断子进程是否被意外关闭 并清理临时文件
        backgroundSaveDoneHandlerDisk(exitcode,bysignal);
        break;
        // 当slave需要同步数据时 产生的rdb子进程类型就是socket类型
    case RDB_CHILD_TYPE_SOCKET:
        // 这里主要就是打印日志
        backgroundSaveDoneHandlerSocket(exitcode,bysignal);
        break;
    default:
        serverPanic("Unknown RDB child type.");
        break;
    }

    // 重置子进程状态 同时更新存储时间
    server.rdb_child_pid = -1;
    server.rdb_child_type = RDB_CHILD_TYPE_NONE;
    server.rdb_save_time_last = time(NULL)-server.rdb_save_time_start;
    server.rdb_save_time_start = -1;
    /* Possibly there are slaves waiting for a BGSAVE in order to be served
     * (the first stage of SYNC is a bulk transfer of dump.rdb) */
    updateSlavesWaitingBgsave((!bysignal && exitcode == 0) ? C_OK : C_ERR, type);
}

/* A background saving child (BGSAVE) terminated its work. Handle this.
 * This function covers the case of actual BGSAVEs.
 * 某个后台存储rdb文件的任务已经完成 这时清理逻辑
 * */
static void backgroundSaveDoneHandlerDisk(int exitcode, int bysignal) {
    // 代表本次处理成功
    if (!bysignal && exitcode == 0) {
        serverLog(LL_NOTICE,
            "Background saving terminated with success");
        // 此时dirty的数值就更新成 仅生成rdb文件期间产生的数据
        server.dirty = server.dirty - server.dirty_before_bgsave;
        server.lastsave = time(NULL);
        server.lastbgsave_status = C_OK;
    } else if (!bysignal && exitcode != 0) {
        serverLog(LL_WARNING, "Background saving error");
        server.lastbgsave_status = C_ERR;
    } else {
        // 代表本次进程由于某种信号被终止
        mstime_t latency;

        serverLog(LL_WARNING,
            "Background saving terminated by signal %d", bysignal);
        latencyStartMonitor(latency);
        // 清理子进程创建的临时文件
        rdbRemoveTempFile(server.rdb_child_pid, 0);
        latencyEndMonitor(latency);
        latencyAddSampleIfNeeded("rdb-unlink-temp-file",latency);
        /* SIGUSR1 is whitelisted, so we have a way to kill a child without
         * triggering an error condition. */
        if (bysignal != SIGUSR1)
            server.lastbgsave_status = C_ERR;
    }
}
复制代码

以上rdb的解析也完成了

文章分类
后端
文章标签