[redis 源码走读] - rdb 持久化 - 应用场景

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析


阶段4、深入jdk其余源码解析


阶段5、深入jvm源码解析

码哥源码部分

码哥讲源码-原理源码篇【2024年最新大厂关于线程池使用的场景题】

码哥讲源码【炸雷啦!炸雷啦!黄光头他终于跑路啦!】

码哥讲源码-【jvm课程前置知识及c/c++调试环境搭建】

​​​​​​码哥讲源码-原理源码篇【揭秘join方法的唤醒本质上决定于jvm的底层析构函数】

码哥源码-原理源码篇【Doug Lea为什么要将成员变量赋值给局部变量后再操作?】

码哥讲源码【你水不是你的错,但是你胡说八道就是你不对了!】

码哥讲源码【谁再说Spring不支持多线程事务,你给我抽他!】

终结B站没人能讲清楚红黑树的历史,不服等你来踢馆!

打脸系列【020-3小时讲解MESI协议和volatile之间的关系,那些将x86下的验证结果当作最终结果的水货们请闭嘴】

rdb 文件是一个经过压缩的二进制文件,是 redis 持久化方式之一。本章主要讲 rdb 应用场景。


1. 配置

redis 有两种持久化方式,分别为:aof 和 rdb,默认开启 rdb,本章重点讲 rdb。

 
    # redis.conf
    appendonly no

 
    standardConfig configs[] = {
        ...
       createBoolConfig("appendonly", NULL, MODIFIABLE_CONFIG, server.aof_enabled, 0, NULL, updateAppendonly),
       ...
    }
    
    void initServer(void) {
        ...
        server.aof_state = server.aof_enabled ? AOF_ON : AOF_OFF;
        ...
    }

2. 异步持久化

redis 主逻辑是在单进程,单线程里实现的。像持久化这种耗大量性能的操作,主进程一般会通过 fork 子进程异步进行。

 
    // 主进程 fork 子进程存盘
    int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
        ...
        if ((childpid = redisFork()) == 0) {
            ...
            /* Child */
            retval = rdbSave(filename,rsi);
            ...
        }
        ...
    }

3. 应用场景

3.1. 服务启动加载数据

redis 程序启动,从磁盘 rdb 文件加载数据到内存。

 
    int main(int argc, char **argv) {
        ...
        if (!server.sentinel_mode) {
            loadDataFromDisk();
        }
    }
    
    /* flags on the purpose of rdb save or load */
    #define RDBFLAGS_NONE 0
    #define RDBFLAGS_AOF_PREAMBLE (1<<0)
    #define RDBFLAGS_REPLICATION (1<<1)
    
    /* Function called at startup to load RDB or AOF file in memory. */
    void loadDataFromDisk(void) {
        long long start = ustime();
        if (server.aof_state == AOF_ON) {
            if (loadAppendOnlyFile(server.aof_filename) == C_OK)
                ...
        } else {
            rdbSaveInfo rsi = RDB_SAVE_INFO_INIT;
            if (rdbLoad(server.rdb_filename,&rsi,RDBFLAGS_NONE) == C_OK) {
                ...
            }
        }
        ...
    }

3.2. 命令

  • SAVE 命令同步存盘。
 
    void saveCommand(client *c) {
        ...
        if (rdbSave(server.rdb_filename,rsiptr) == C_OK) {
            addReply(c,shared.ok);
        } else {
            addReply(c,shared.err);
        }
    }
  • BGSAVE 命令,主进程通过 fork 子进程进行异步存盘。
 
    void bgsaveCommand(client *c) {
        ...
        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 {
                ...
            }
        } else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) {
            addReplyStatus(c,"Background saving started");
        }
        ...
    }
  • FLUSHALL 清空数据库后存盘。
 
    void flushallCommand(client *c) {
        ...
        flushAllDataAndResetRDB(flags);
        ...
    }
    
    /* Flushes the whole server data set. */
    void flushAllDataAndResetRDB(int flags) {
        server.dirty += emptyDb(-1,flags,NULL);
        if (server.rdb_child_pid != -1) killRDBChild();
        if (server.saveparamslen > 0) {
            /* Normally rdbSave() will reset dirty, but we don't want this here
             * as otherwise FLUSHALL will not be replicated nor put into the AOF. */
            int saved_dirty = server.dirty;
            rdbSaveInfo rsi, *rsiptr;
            rsiptr = rdbPopulateSaveInfo(&rsi);
            rdbSave(server.rdb_filename,rsiptr);
            server.dirty = saved_dirty;
        }
        server.dirty++;
        ...
    }
  • SHUTDOWN 命令关闭服务。 服务运行过程中,一般情况是通过定期策略对内存数据进行持久化,内存数据和持久化文件数据不同步的,所以当服务正常退出或者重启,需要将内存数据进行持久化。
 
    void shutdownCommand(client *c) {
        ...
        if (prepareForShutdown(flags) == C_OK) exit(0);
        ...
    }
    
    int prepareForShutdown(int flags) {
        ...
        /* Create a new RDB file before exiting. */
        if ((server.saveparamslen > 0 && !nosave) || save) {
            ...
            rdbSaveInfo rsi, *rsiptr;
            rsiptr = rdbPopulateSaveInfo(&rsi);
            if (rdbSave(server.rdb_filename,rsiptr) != C_OK) {
                ...
            }
        }
        ...
    }

3.3. 数据定期持久化

rdb 持久化是有条件限制的:

  1. 数据修改个数。
  2. 存盘时间间隔。
  • 默认配置 从默认配置看,rdb 持久化不是实时的。时间间隔,最大 900 秒(15 分钟),最小 60 秒(1分钟),所以用 rdb 做持久化丢失数据风险比较大。
 
    # redis.conf
    ################################ SNAPSHOTTING  ################################
    #
    # Save the DB on disk:
    #
    #   save <seconds> <changes>
    #
    #   Will save the DB if both the given number of seconds and the given
    #   number of write operations against the DB occurred.
    #
    #   In the example below the behaviour will be to save:
    #   after 900 sec (15 min) if at least 1 key changed
    #   after 300 sec (5 min) if at least 10 keys changed
    #   after 60 sec if at least 10000 keys changed
    #
    #   Note: you can disable saving completely by commenting out all "save" lines.
    #
    #   It is also possible to remove all the previously configured save
    #   points by adding a save directive with a single empty string argument
    #   like in the following example:
    #
    #   save ""
    
    save 900 1
    save 300 10
    save 60 10000
 
    // rdb 定期存盘参数
    struct saveparam {
        time_t seconds; // 时间间隔
        int changes;    // 修改次数
    };
  • 时钟定期检查将内存数据进行 rdb 持久化。
 
    #define CONFIG_BGSAVE_RETRY_DELAY 5 /* Wait a few secs before trying again. */
    
    struct redisServer {
        ...
        long long dirty;                /* Changes to DB from the last save */
        time_t lastsave;                /* Unix time of last successful save */
        time_t lastbgsave_try;          /* Unix time of last attempted bgsave */
        ...
    }
    
    int hasActiveChildProcess() {
        return server.rdb_child_pid != -1 ||
               server.aof_child_pid != -1 ||
               server.module_child_pid != -1;
    }
    
    int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
        ...
        if (hasActiveChildProcess() || ldbPendingChildren()) {
            // 如果后台有子进程正在进行活动,检查进程是否已经终止。
            checkChildrenDone();
        } else {
            for (j = 0; j < server.saveparamslen; j++) {
                struct saveparam *sp = server.saveparams+j;
    
                // 需要满足默认数据保存频率条件。
                // 如果上次存盘失败后,需要延时 CONFIG_BGSAVE_RETRY_DELAY 再进行操作。
                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 命令时,当时有其它子进程正在进行工作,所以该命令被安排延后处理。
        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;
        }
        ...
    }

3.4. 重写 aof 文件

aof 文件在重写过程中,为了快速将数据落地,也会将文件保存成 rdb 文件,rdb 文件里会保存 aof 标识进行识别。

 
    # redis.conf
    #
    # When rewriting the AOF file, Redis is able to use an RDB preamble in the
    # AOF file for faster rewrites and recoveries. When this option is turned
    # on the rewritten AOF file is composed of two different stanzas:
    #
    #   [RDB file][AOF tail]
    #
    # When loading Redis recognizes that the AOF file starts with the "REDIS"
    # string and loads the prefixed RDB file, and continues loading the AOF
    # tail.
    aof-use-rdb-preamble yes
 
    // 重写 aof 文件
    int rewriteAppendOnlyFile(char *filename) {
        ...
        startSaving(RDBFLAGS_AOF_PREAMBLE);
        if (server.aof_use_rdb_preamble) {
            int error;
            if (rdbSaveRio(&aof,&error,RDBFLAGS_AOF_PREAMBLE,NULL) == C_ERR) {
                errno = error;
                goto werr;
            }
        }
        ...
    }
    
    // 加载 aof 文件
    int loadAppendOnlyFile(char *filename) {
        ...
        char sig[5]; /* "REDIS" */
        if (fread(sig,1,5,fp) != 5 || memcmp(sig,"REDIS",5) != 0) {
            /* No RDB preamble, seek back at 0 offset. */
            if (fseek(fp,0,SEEK_SET) == -1) goto readerr;
        } else {
            ...
            // 从 rdb 文件加载 aof 需要的数据。
            if (rdbLoadRio(&rdb,RDBFLAGS_AOF_PREAMBLE,NULL) != C_OK) {
                ...
            }
            ...
        }
        ...
    }

3.5. 信号终止进程

服务运行过程中,一般情况是通过定期策略对内存数据进行持久化,内存数据和持久化文件数据不同步的,所以当服务正常退出或者重启,需要将内存数据进行持久化。

 
    void initServer(void) {
        ...
        setupSignalHandlers();
        ...
    }
    
    #define SIGINT  2       /* interrupt */
    #define SIGTERM 15      /* software termination signal from kill */
    
    void setupSignalHandlers(void) {
        struct sigaction act;
    
        /* When the SA_SIGINFO flag is set in sa_flags then sa_sigaction is used.
         * Otherwise, sa_handler is used. */
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        act.sa_handler = sigShutdownHandler;
        sigaction(SIGTERM, &act, NULL);
        sigaction(SIGINT, &act, NULL);
        ...
    }
    
    static void sigShutdownHandler(int sig) {
        ...
        server.shutdown_asap = 1;
    }
    
    int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
        ...
        /* We received a SIGTERM, shutting down here in a safe way, as it is
         * not ok doing so inside the signal handler. */
        if (server.shutdown_asap) {
            if (prepareForShutdown(SHUTDOWN_NOFLAGS) == C_OK) exit(0);
            serverLog(LL_WARNING,"SIGTERM received but errors trying to shut down the server, check the logs for more information");
            server.shutdown_asap = 0;
        }
        ...
    }

3.6. 主从复制

主从复制,全量同步数据,可以通过 rdb 文件传输。rdb 文件可以采用硬盘备份方式;也可以无盘备份,数据不存盘,直接通过 socket 发送给其它服务。

从服务刚启动或因网络原因,与主服务长时间断开,重连后发现主从数据已经严重不匹配了,主服务需要将内存数据保存成 rdb 二进制压缩文件,传送给这些重新链接的服务。

一主多从架构,如果出现网络问题,极端情况,主服务要给多个从服务发送 rdb 文件数据,数据量大的话,可能会造成网络拥堵,所以从服务尽量少吧。如果应用场景确实需要,可以启用多级从服务(chained slaves (slaves of slaves)),避免主服务出现过载问题。

 
    /* State of slaves from the POV of the master. Used in client->replstate.
     * In SEND_BULK and ONLINE state the slave receives new updates
     * in its output queue. In the WAIT_BGSAVE states instead the server is waiting
     * to start the next background saving in order to send updates to it. */
    #define SLAVE_STATE_WAIT_BGSAVE_START 6 /* We need to produce a new RDB file. */
    #define SLAVE_STATE_WAIT_BGSAVE_END 7 /* Waiting RDB file creation to finish. */
    #define SLAVE_STATE_SEND_BULK 8 /* Sending RDB file to slave. */
    #define SLAVE_STATE_ONLINE 9 /* RDB file transmitted, sending just updates. */
    
    void syncCommand(client *c) {
        ...
        /* Setup the slave as one waiting for BGSAVE to start. The following code
         * paths will change the state if we handle the slave differently. */
        c->replstate = SLAVE_STATE_WAIT_BGSAVE_START;
        ...
    }
 
    void replicationCron(void) {
        ...
        /* 如果使用无硬盘备份,主服务会在开始传送前等待一段时间(repl_diskless_sync_delay),
        这过程中可能有多个服务链接上来需要全量同步数据的,那么一起同步。*/
        if (!hasActiveChildProcess()) {
            time_t idle, max_idle = 0;
            int slaves_waiting = 0;
            int mincapa = -1;
            listNode *ln;
            listIter li;
    
            // 遍历从服务,确认是否需要主从复制。
            listRewind(server.slaves,&li);
            while((ln = listNext(&li))) {
                client *slave = ln->value;
                if (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_START) {
                    idle = server.unixtime - slave->lastinteraction;
                    if (idle > max_idle) max_idle = idle;
                    slaves_waiting++;
                    mincapa = (mincapa == -1) ? slave->slave_capa :
                                                (mincapa & slave->slave_capa);
                }
            }
    
            if (slaves_waiting &&
                (!server.repl_diskless_sync ||
                 max_idle > server.repl_diskless_sync_delay)) {
                startBgsaveForReplication(mincapa);
            }
        }
        ...
    }
    
    int startBgsaveForReplication(int mincapa) {
        ...
        if (rsiptr) {
            if (socket_target)
                retval = rdbSaveToSlavesSockets(rsiptr);
            else
                retval = rdbSaveBackground(server.rdb_filename,rsiptr);
        }
        ...
    }

4. 总结

rdb 作为持久化方式的一种,它是一种经过压缩的二进制数据。

  • 优点:持久化过程中,速度快,文件体积小。方便数据快速落地,或者通过网络传输数据。

  • 缺点:

    1. redis 只是将 rdb 文件作为一个备份文件而已,功能简单,并不能从文件中做一些数据查询功能操作。
    2. 备份常用方式是通过时钟控制,不是实时的,异常情况丢失数据会比较多。如果把它作为一个数据库来应用,这显然是不能接受的。

 

  • 14
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值