redis——RDF持久化

redis是内存数据库,如果数据没有写入磁盘,一旦服务器挂掉或者进程退出,那么数据也就不见了。

redis通过将数据保存到一个RDB文件中来实现持久化。

RDB文件是一个经过压缩的二进制文件,保存在硬盘上,这样即便是服务器挂掉或者进程退出,数据也不会丢失。在服务器启动时会载入RDB文件。


先来看看创建RDB文件

redis提供了两种命令来创建RDB文件:SAVE命令和BGSAVE命令。

SAVE

客户端输入SAVE命令

void saveCommand(client *c) {
    /* 判断是否已经有bgsave操作 */
    if (server.rdb_child_pid != -1) {
        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 {
        addReply(c,shared.err);
    }
}

这里关注rdbSave方法,就是SAVE命令的具体实现:

int rdbSave(char *filename, rdbSaveInfo *rsi) {
    char tmpfile[256];
    char cwd[MAXPATHLEN]; /* 存放错误信息 */
    FILE *fp;
    rio rdb;
    int error = 0;

    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
    fp = fopen(tmpfile,"w"); /* 调用函数库的fopen函数,创建(打开)只写文件 */
    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);
    /* 以RDB格式生成数据库转储文件,并将其发送到指定的Redis I/O通道 */
    if (rdbSaveRio(&rdb,&error,RDB_SAVE_NONE,rsi) == C_ERR) {
        errno = error;
        goto werr;
    }

    /* 确保数据不会留在操作系统的输出缓冲区中 */
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;
    if (fclose(fp) == EOF) goto werr;

    /* 调用函数库的rename方法 */
    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;
}

其中涉及到rio对象的一些操作。redis将I/O层封装起来成rio对象,文件读写和buffer都基于它来操作。这里不展开来说。


BGSAVE

客户端输入BGSAVE(background save)命令

void bgsaveCommand(client *c) {
    int schedule = 0;

    /* 校验schedule参数 */
    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 (server.aof_child_pid != -1) {/* 如果这时有AOF操作 */
        if (schedule) {/* 给schedule参数重新赋值 */
            server.rdb_bgsave_scheduled = 1;
            addReplyStatus(c,"Background saving scheduled");
        } else {/* 否则抛错:AOF和BGSAVE不能同时进行 */
            addReplyError(c,
                "An AOF log rewriting in progress: 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) {/* BGSAVE */
        addReplyStatus(c,"Background saving started");
    } else {
        addReply(c,shared.err);
    }
}

这里关注rdbSaveBackground方法:

int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
    pid_t childpid;
    long long start;

    if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;

    server.dirty_before_bgsave = server.dirty;
    server.lastbgsave_try = time(NULL);
    /* 打开子-父通道 */
    openChildInfoPipe();

    start = ustime();
    /* fork出子进程 */
    if ((childpid = fork()) == 0) {
        int retval;

        /* 以下是子进程的操作 */
        closeListeningSockets(0);
        redisSetProcTitle("redis-rdb-bgsave");
        /* 由子进程去完成save操作 */
        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 {
        /* 以下是父进程的操作 */
        server.stat_fork_time = ustime()-start;
        server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); 
        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; 
}

 


通过SAVE和BGSAVE的实现可以看出,SAVE命令会阻塞服务器直到RDB文件创建完毕,但是BGSAVE命令会fork出新的子进程来负责RDB文件的创建,而父进程也就是服务器进程可以继续处理命令请求,不会被阻塞(可以参考java中的fork-join框架)。

因此,可以通过设置save选项,让服务器每隔一段时间自动的执行一次BGSAVE。

自动间隔保存

在redis.conf中有这么一段:

redis——RDF持久化_第1张图片

这里默认设置了三个条件,他们的意思分别是:

  1. 服务器在900秒内对数据库做了至少1次修改
  2. 服务器在300秒内对数据库做了至少10次修改
  3. 服务器在60秒内对数据库做了至少10000次修改

只要这三个条件中的任意一条符合,那么服务器就会执行BGSAVE。

这些默认条件会保存在redisServer结构的saveparam数组中:

struct redisServer {
    long long dirty;
    time_t lastsave;
    struct saveparam *saveparams;
    //...
};

saveparam结构如下:

struct saveparam {
    time_t seconds; /* 秒 */
    int changes;    /* 修改次数 */
};

 redisServer中还有两个属性:

dirty:记录上一次SAVE或者BGSAVE之后,服务器对所有的数据库做了多少次修改;

lastsave:记录了上一次服务器成功执行SAVE或BGSAVE的时间。

服务器会周期性执行serverCron函数,如下,这个函数会去检查此时是否满足save选项设置的条件,如果满足那么就会执行BGSAVE:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    //...

    if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
        ldbPendingChildren())
    {
        //...
    } else {
         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);
                /* 满足条件,执行BGSAVE */
                rdbSaveInfo rsi, *rsiptr;
                rsiptr = rdbPopulateSaveInfo(&rsi);
                rdbSaveBackground(server.rdb_filename,rsiptr);
                break;
            }
         }
      //...
    }
    //...
    return 1000/server.hz;
}

再来看看载入RDB文件

int rdbLoad(char *filename, rdbSaveInfo *rsi) {
    FILE *fp;
    rio rdb;
    int retval;

    if ((fp = fopen(filename,"r")) == NULL) return C_ERR;
    startLoading(fp);
    rioInitWithFile(&rdb,fp);
    retval = rdbLoadRio(&rdb,rsi,0);
    fclose(fp);
    stopLoading();
    return retval;
}

通过rio对象来操作load。可以看出load操作会阻塞服务器直到load完毕。

 

你可能感兴趣的:(redis源码学习)