redis源码学习-持久化rdb篇

redis数据持久化有多种方案,不落盘的方式可以使用主从,落盘的方式有rdb,aof或者rdb+aof
本篇详细介绍rdb持久化方式

rdb处理流程

在redis中,rdb为快照持久化方式。特点比较明显,即在某一时间点将当前数据全部保存下来。
rdb主要也分为两种,主动快照或被动快照。

主动触发

主要通过save与bgsave两条命令。bg指的是background,bgsave为异步的save命令。redis在数据处理层面上依然为单线程,因此简单的save在备份时会导致redis节点不可用,这也是bgsave的由来。无论生产或测试环境都推荐bgsave不使用save,save命令也建议通过rename进行屏蔽。对于线上环境来说,save的危险性不亚于keys *。

被动触发

主要通过conf文件中的save inta intb。指的是每过inta秒,数据变化大于intb时自动调用bgsave。同时save支持多行写入,例如写两行 save 10 20 save 900 1 ,当redis满足任意一条save条件时都会调用bgsave进行持久化。
如上所述rdb的处理流程,可见redis的rdb机制本身是定时备份的大框架。在下一个备份周期到来时,数据的可靠性是没有保障的。

bgsave处理流程

  1. 判断是否在save的过程中,若则跳过此次
  2. fork出子进程
  3. 打开一个临时文件
  4. 将当前redis的内存信息写入到这个临时文件中
  5. 将文件写入磁盘中
  6. 将临时文件改名为正式的RDB文件
  7. 记录 dirty 和 lastsave等状态
    1-7步不为完整的事务状态,当rdb正式改名后redis发生故障,dirty与lastsave数据不对时,仍可通过第四步的rdb文件进行数据的恢复。同时也可看出,每个bgsave的过程中都互不干涉。对于大数据量节点的情况下,较为耗时的为4,5步,若save配置太过频繁,导致redis每时刻都在save的过程中,对系统负载有所影响。但同时save配置频率太低,也会导致数据的不可靠性增加,因此在进行redis扩容或缩容时,也要根据相应的数据量调整redisRDB的参数设置。

rdb源码分析

rio

struct _rio {
    /* Backend functions.
     * Since this functions do not tolerate short writes or reads the return
     * value is simplified to: zero on error, non zero on complete success. */
    size_t (*read)(struct _rio *, void *buf, size_t len);
    size_t (*write)(struct _rio *, const void *buf, size_t len);
    off_t (*tell)(struct _rio *);
    int (*flush)(struct _rio *);
    /* The update_cksum method if not NULL is used to compute the checksum of
     * all the data that was read or written so far. The method should be
     * designed so that can be called with the current checksum, and the buf
     * and len fields pointing to the new block of data to add to the checksum
     * computation. */
    void (*update_cksum)(struct _rio *, const void *buf, size_t len);

    /* The current checksum and flags (see RIO_FLAG_*) */
    uint64_t cksum, flags;

    /* number of bytes read or written */
    size_t processed_bytes;

    /* maximum single read or write chunk size */
    size_t max_processing_chunk;

    /* Backend-specific vars. */
    union {
        /* In-memory buffer target. */
        struct {
            sds ptr;
            off_t pos;
        } buffer;
        /* Stdio file pointer target. */
        struct {
            FILE *fp;
            off_t buffered; /* Bytes written since last fsync. */
            off_t autosync; /* fsync after 'autosync' bytes written. */
        } file;
        /* Connection object (used to read from socket) */
        struct {
            connection *conn;   /* Connection */
            off_t pos;    /* pos in buf that was returned */
            sds buf;      /* buffered data */
            size_t read_limit;  /* don't allow to buffer/read more than that */
            size_t read_so_far; /* amount of data read from the rio (not buffered) */
        } conn;
        /* FD target (used to write to pipe). */
        struct {
            int fd;       /* File descriptor. */
            off_t pos;
            sds buf;
        } fd;
    } io;
};

rio为redis对io的一层包装,可以理解为redisio,抽象出read,write等方法,在rdb或aof时通过rio真正对操作系统的io进行操作。

rdbSave

/* Save the DB on disk. Return C_ERR on error, C_OK on success. */
int rdbSave(char *filename, rdbSaveInfo *rsi) {
    //临时文件,用于库函数snprintf
    char tmpfile[256];
    //找到当前工作目录
    char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */
    FILE *fp = NULL;
    rio rdb;
    int error = 0;
    //库函数,用于生成临时文件,以temp-开头,整体完成后再进行改名
    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
    //打开文件
    fp = fopen(tmpfile,"w");
    //打开失败时写日志,固定格式Failed opening the RDB file......
    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;
    }
    //初始化rio数据结构,用来与操作系统进行io交互
    rioInitWithFile(&rdb,fp);
    startSaving(RDBFLAGS_NONE);
    //参数判断,通过分批将数据fsync到硬盘,用来缓冲io,建议开启
    if (server.rdb_save_incremental_fsync)
        rioSetAutoSync(&rdb,REDIS_AUTOSYNC_BYTES);

    if (rdbSaveRio(&rdb,&error,RDBFLAGS_NONE,rsi) == C_ERR) {
        errno = error;
        goto werr;
    }

    /* Make sure data will not remain on the OS's output buffers */
    if (fflush(fp)) goto werr;
    if (fsync(fileno(fp))) goto werr;
    if (fclose(fp)) { fp = NULL; goto werr; }
    fp = NULL;
    
    /* Use RENAME to make sure the DB file is changed atomically only
     * if the generate DB file is ok. */
    //重命名操作,修改temp-*.rdb为真正的.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);
        stopSaving(0);
        return C_ERR;
    }
    //日志写入,固定格式DB saved on disk
    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:
    //异常写入日志,固定格式Write error saving DB on disk
    serverLog(LL_WARNING,"Write error saving DB on disk: %s", strerror(errno));
    if (fp) fclose(fp);
    unlink(tmpfile);
    stopSaving(0);
    return C_ERR;
}

rdbSave为真正的落盘操作,在bgsave中被rdbSaveBackground作为子流程完整执行

rdbSaveBackground

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);
    //fock子进程,redis仅作包装,真正的fork还是依托于操作系统
    if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) {
        int retval;

        /* Child */
        //子进程操作
        redisSetProcTitle("redis-rdb-bgsave");
        redisSetCpuAffinity(server.bgsave_cpulist);
        //将rdbSave完整的作为子流程
        retval = rdbSave(filename,rsi);
        if (retval == C_OK) {
            sendChildCowInfo(CHILD_INFO_TYPE_RDB_COW_SIZE, "RDB");
        }
        exitFromChild((retval == C_OK) ? 0 : 1);
    } else {
        /* Parent */
        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 */
}

主要依托操作系统的fork,在子进程中完整调用rdbSave进行持久化

serverCron(自动rdb持久化核心函数,在server.c中)

核心代码
 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调用的为rdbSaveBackground,在bgsave前会先判断是否正在bgsave,防止出现大量fork子进程导致灾难。

rdb参数

rdb_save_incremental_fsync

yes时redis通过分批将数据fsync到硬盘,用来缓冲io,保证每次fsync数据量在32m以内。建议开启,redis5之后支持

stop-writes-on-bgsave-error

当bgsave快照操作出错时停止写数据到磁盘。建议为no

rdbcompression

是否进行rdb压缩

rdbchecksum

是否检查rdb
相对于数据库来说,rdb的持久化方式相对太过粗糙,fork进程时也总会让人担心是否会造成redis的卡顿,但在数据恢复时相对来说较为友好。相对于aof的方式,rdb的数据安全性得不到更好的保障,在bgsave时也需额外内存进行数据复制。对于线上来说,在意数据安全性的话不建议只开rdb,推荐aof或aof+rdb的方式,至少aof的类binlog记录方式对于故障溯源相对友好。
同时rdb文件的可读性也较差,需通过od -A -x -t xlc -v dump.rdb将rdb文件转化为16进制后才能进行阅读。