Redis源码剖析和注释(十七)--- RDB持久化机制

Redis RDB持久化机制

1. RDB的介绍

因为Redis是内存数据库,因此将数据存储在内存中,如果一旦服务器进程退出,服务器中的数据库状态就会消失不见,为了解决这个问题,Redis提供了两种持久化的机制:RDBAOF。本篇主要剖析RDB持久化的过程。

RDB持久化是把当前进程数据生成时间点快照(point-in-time snapshot)保存到硬盘的过程,避免数据意外丢失。

1.1 RDB触发机制

RDB触发机制分为手动触发和自动触发

  • 手动触发的两条命令:

    • SAVE:阻塞当前Redis服务器,知道RDB过程完成为止。
    • BGSAVE:Redis 进程执行fork()操作创建出一个子进程,在后台完成RDB持久化的过程。(主流)
  • 自动触发的配置:

    • c
      save 900 1 //服务器在900秒之内,对数据库执行了至少1次修改
      save 300 10 //服务器在300秒之内,对数据库执行了至少10修改
      save 60 1000 //服务器在60秒之内,对数据库执行了至少1000修改
      // 满足以上三个条件中的任意一个,则自动触发 BGSAVE 操作
      // 或者使用命令CONFIG SET 命令配置

1.2 RDB持久化的流程

我们用图来表示 BGSAVE命令 的触发流程,如下图所示:

这里写图片描述

RDB命令源码如下:Redis 3.2 RDB源码注释

/* BGSAVE [SCHEDULE] */
// BGSAVE 命令实现
void bgsaveCommand(client *c) {
    int schedule = 0;   //SCHEDULE控制BGSAVE的执行,避免和AOF重写进程冲突

    /* 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) {
        // 设置schedule标志
        if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) {
            schedule = 1;
        } else {
            addReply(c,shared.syntaxerr);
            return;
        }
    }

    // 如果正在执行RDB持久化操作,则退出
    if (server.rdb_child_pid != -1) {
        addReplyError(c,"Background save already in progress");

    // 如果正在执行AOF持久化操作,需要将BGSAVE提上日程表
    } else if (server.aof_child_pid != -1) {
        // 如果schedule为真,设置rdb_bgsave_scheduled为1,表示将BGSAVE提上日程表
        if (schedule) {
            server.rdb_bgsave_scheduled = 1;
            addReplyStatus(c,"Background saving scheduled");
        } else {    //没有设置schedule,则不能立即执行BGSAVE
            addReplyError(c,
                "An AOF log rewriting in progress: can't BGSAVE right now. "
                "Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenver "
                "possible.");
        }

    // 执行BGSAVE
    } else if (rdbSaveBackground(server.rdb_filename) == C_OK) {
        addReplyStatus(c,"Background saving started");
    } else {
        addReply(c,shared.err);
    }
}

我们后面会重点讲解rdbSaveBackground()函数的工作过程。

1.3 RDB的优缺点

RDB的优点:

  • RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据快照。非常适用于备份,全景复制等场景。
  • Redis 加载RDB恢复数据远远快于AOF的方式。

RDB的缺点:

  • RDB没有办法做到实时持久化或秒级持久化。因为BGSAVE每次运行的又要进行fork()的调用创建子进程,这属于重量级操作,频繁执行成本过高,因为虽然Linux支持读时共享,写时拷贝(copy-on-write)的技术,但是仍然会有大量的父进程的空间内存页表,信号控制表,寄存器资源等等的复制。
  • RDB文件使用特定的二进制格式保存,Redis版本演进的过程中,有多个RDB版本,这导致版本兼容的问题。

2. RDB 的源码剖析

阅读此部分,可以跳过源码,只看文字部分,因为所有过程的依据我都以源码的方式给出,因此篇幅会比较长,但是我都以文字解释,所以可以跳过源码,只读文字,理解RDB的过程。也可以上github查看所有代码的注释:Redis 3.2 源码注释

之前我们给出了 BGSAVE命令 的源码,因此我们就重点剖析 rdbSaveBackground()的工作过程,一层一层的剥开封装。

RDB持久化之前需要设置一些标识,用来标识服务器当前的状态,定义在server.h/struct redisServer 结构体中,我们列出会用到的一部分,如果需要可以在这里查看。Redis 3.2 源码注释

struct redisServer {
    // 数据库数组,长度为16
    redisDb *db;
    // 从节点列表和监视器列表
    list *slaves, *qiank;    /* List of slaves and MONITORs */

    /* RDB / AOF loading information ××××××××××××××××××××××××××××××××××××××××××××××××××××××××××*/
    // 正在载入状态
    int loading;                /* We are loading data from disk if true */

    // 设置载入的总字节
    off_t loading_total_bytes;

    // 已载入的字节数
    off_t loading_loaded_bytes;

    // 载入的开始时间
    time_t loading_start_time;

    // 在load时,用来设置读或写的最大字节数max_processing_chunk
    off_t loading_process_events_interval_bytes;

    // 服务器内存使用的
    size_t stat_peak_memory;        /* Max used memory record */

    // 计算fork()的时间
    long long stat_fork_time;       /* Time needed to perform latest fork() */

    // 计算fork的速率,GB/每秒
    double stat_fork_rate;          /* Fork rate in GB/sec. */

    /* RDB persistence ××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××*/
    // 脏键,记录数据库被修改的次数
    long long dirty;                /* Changes to DB from the last save */

    // 在BGSAVE之前要备份脏键dirty的值,如果BGSAVE失败会还原
    long long dirty_before_bgsave;  /* Used to restore dirty on failed BGSAVE */

    // 执行BGSAVE的子进程的pid
    pid_t rdb_child_pid;            /* PID of RDB saving child */

    // 保存save参数的数组
    struct saveparam *saveparams;   /* Save points array for RDB */

    // 数组长度
    int saveparamslen;              /* Number of saving points */

    // RDB文件的名字,默认为dump.rdb
    char *rdb_filename;             /* Name of RDB file */

    // 是否采用LZF压缩算法压缩RDB文件,默认yes
    int rdb_compression;            /* Use compression in RDB? */

    // RDB文件是否使用校验和,默认yes
    int rdb_checksum;               /* Use RDB checksum? */

    // 上一次执行SAVE成功的时间
    time_t lastsave;                /* Unix time of last successful save */

    // 最近一个尝试执行BGSAVE的时间
    time_t lastbgsave_try;          /* Unix time of last attempted bgsave */

    // 最近执行BGSAVE的时间
    time_t rdb_save_time_last;      /* Time used by last RDB save run. */

    // BGSAVE开始的时间
    time_t rdb_save_time_start;     /* Current RDB save start time. */

    // 当rdb_bgsave_scheduled为真时,才能开始BGSAVE
    int rdb_bgsave_scheduled;       /* BGSAVE when possible if true. */

    // rdb执行的类型,是写入磁盘,还是写入从节点的socket
    int rdb_child_type;             /* Type of save by active child. */

    // BGSAVE执行完的状态
    int lastbgsave_status;          /* C_OK or C_ERR */

    // 如果不能执行BGSAVE则不能写
    int stop_writes_on_bgsave_err;  /* Don't allow writes if can't BGSAVE */

    // 无磁盘同步,管道的写端
    int rdb_pipe_write_result_to_parent; /* RDB pipes used to return the state */
    // 无磁盘同步,管道的读端
    int rdb_pipe_read_result_from_child; /* of each slave in diskless SYNC. */

    /* time cache ××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××*/
    // 保存秒单位的Unix时间戳的缓存
    time_t unixtime;        /* Unix time sampled every cron cycle. */

    // 保存毫秒单位的Unix时间戳的缓存
    long long mstime;       /* Like 'unixtime' but with milliseconds resolution. */

    /* Latency monitor ××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××*/
    // 延迟的阀值
    long long latency_monitor_threshold;
    // 延迟与造成延迟的事件关联的字典
    dict *latency_events;
};

然后我们直接给rdbSaveBackground()函数出源码:

在这里,就可以看见fork()函数的执行,在子进程中执行了rdbSave()函数,父进程则执行了一些设置状态的操作。

// 后台进行RDB持久化BGSAVE操作
int rdbSaveBackground(char *filename) {
    pid_t childpid;
    long long start;

    // 当前没有正在进行AOF和RDB操作,否则返回C_ERR
    if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;

    // 备份当前数据库的脏键值
    server.dirty_before_bgsave = server.dirty;
    // 最近一个执行BGSAVE的时间
    server.lastbgsave_try = time(NULL);
    // fork函数开始时间,记录fork函数的耗时
    start = ustime();
    // 创建子进程
    if ((childpid = fork()) == 0) {
        int retval;
        // 子进程执行的代码
        /* Child */

        // 关闭监听的套接字
        closeListeningSockets(0);
        // 设置进程标题,方便识别
        redisSetProcTitle("redis-rdb-bgsave");
        // 执行保存操作,将数据库的写到filename文件中
        retval = rdbSave(filename);

        if (retval == C_OK) {
            // 得到子进程进程的脏私有虚拟页面大小,如果做RDB的同时父进程正在写入的数据,那么子进程就会拷贝一个份父进程的内存,而不是和父进程共享一份内存。
            size_t private_dirty = zmalloc_get_private_dirty();
            // 将子进程分配的内容写日志
            if (private_dirty) {
                serverLog(LL_NOTICE,
                    &#
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值