头图
公众号搜索“码路印记”

写在前面

Redis是一个内存数据库,也就是说所有的数据将保存在内存中,这与传统的MySQL、Oracle、SqlServer等关系型数据库直接把数据保存到硬盘相比,Redis的读写效率非常高。但是保存在内存中也有一个很大的缺陷,一旦断电或者宕机,内存数据库中的内容将会全部丢失。为了弥补这一缺陷,Redis提供了把内存数据持久化到硬盘文件,以及通过备份文件来恢复数据的功能。

Redis支持两种方式的持久化:RDB快照文件和AOF。本文先介绍RDB快照方式的工作原理、优缺点等方面进行阐述,写完AOF方式后再对比两者的优缺点,结合前辈总结给出生产实践,希望能够对你理解Redis的持久化机制带来帮助。通过本文你将了解到以下内容:~~
image.png
RDB快照用官方的话来说:RDB持久化方案是按照指定时间间隔对你的数据集生成的时间点快照。它是Redis数据库中数据的内存快照,它是一个二进制文件(默认名称为:dump.rdb,可修改),存储了文件生成时Redis数据库中所有的数据内容。可用于Redis的数据备份、转移与恢复。

配置参数

RDB快照的触发方式及运行行为受配置参数的影响,打开配置文件redis.conf查看“SNAPSHOTTING”章节,了解RDB快照的参数及作用。对于各个参数的含义进行了翻译,英语好的同学可以直接看英文。

  • save <seconds> <changes>
################################ 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 behavior 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

save参数是Redis触发自动备份的触发策略,seconds为统计时间(单位:秒), changes为在统计时间内发生写入的次数。save m n的意思是:m秒内有n条写入就触发一次快照,即备份一次。save参数可以配置多组,满足在不同条件的备份要求。如果需要关闭RDB的自动备份策略,可以使用save ""。以下为几种配置的说明:

save 900 1:表示900秒(15分钟)内至少有1个key的值发生变化,则执行
save 300 10:表示300秒(5分钟)内至少有1个key的值发生变化,则执行
save 60 10000:表示60秒(1分钟)内至少有10000个key的值发生变化,则执行
save "": 该配置将会关闭RDB方式的持久化
  • dbfilename:快照文件的名称,默认为dump.rdb。
  • dir:快照文件保存目录,默认与当前配置文件在同一目录。
  • stop-writes-on-bgsave-error:默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难(disaster)发生了。如果Redis重启了,那么又可以重新开始接收数据了。
# By default Redis will stop accepting writes if RDB snapshots are enabled
# (at least one save point) and the latest background save failed.
# This will make the user aware (in a hard way) that data is not persisting
# on disk properly, otherwise chances are that no one will notice and some
# disaster will happen.
#
# If the background saving process will start working again Redis will
# automatically allow writes again.
#
# However if you have setup your proper monitoring of the Redis server
# and persistence, you may want to disable this feature so that Redis will
# continue to work as usual even if there are problems with disk,
# permissions, and so forth.
stop-writes-on-bgsave-error yes
  • rdbcompression:默认值yes。备份数据时是否使用LZF算法压缩字符串对象。默认是开启的,这样可以节约存储空间,但是在生成备份文件时消耗部分CPU。
# Compress string objects using LZF when dump .rdb databases?
# By default compression is enabled as it's almost always a win.
# If you want to save some CPU in the saving child set it to 'no' but
# the dataset will likely be bigger if you have compressible values or keys.
rdbcompression yes
  • rdbchecksum:保存和加载rdb文件时是否使用CRC64校验,默认开启。启用此参数可以使rdb文件更加安全,提高稳定性,但是会有一定的性能(大约10%)损失。如果rdb文件创建时未使用校验和,那么校验和将被设置为0,以此告知Redis跳过校验。
# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.
# This makes the format more resistant to corruption but there is a performance
# hit to pay (around 10%) when saving and loading RDB files, so you can disable it
# for maximum performances.
#
# RDB files created with checksum disabled have a checksum of zero that will
# tell the loading code to skip the check.
rdbchecksum yes

持久化流程

在Redis内完成RDB持久化的方法有rdbSave和rdbSaveBackground两个函数方法(源码文件rdb.c中),先简单说下两者差别:

  • rdbSave:是同步执行的,方法调用后就会立刻启动持久化流程。由于Redis是单线程模型,持久化过程中会阻塞,Redis无法对外提供服务;
  • rdbSaveBackground:是后台(异步)执行的,该方法会fork出子进程,真正的持久化过程是在子进程中执行的,主进程会继续提供服务;

RDB持久化的触发必然离不开以上两个方法,触发的方式分为手动和自动。手动触发容易理解,是指我们通过Redis客户端人为的对Redis服务端发起持久化备份指令,然后Redis服务端开始执行持久化流程,这里的指令有save和bgsave。自动触发是Redis根据自身运行要求,在满足预设条件时自动触发的持久化流程,自动触发的场景有如下几个(摘自这篇文章):

  • serverCron中save m n配置规则自动触发;
  • 从节点全量复制时,主节点发送rdb文件给从节点完成复制操作,主节点会出发bgsave;
  • 执行debug reload命令重新加载redis时;
  • 默认情况下(未开启AOF)执行shutdown命令时,自动执行bgsave;

结合源码及参考文章,我整理了RDB持久化流程来帮助大家有个整体的了解,然后再从一些细节进行说明。从下图可以知道,自动触发流程是一个完整的链路,涵盖了rdbSaveBackground、rdbSave等,接下来我以serverCron为例分析一下整个流程。
image.png

serverCron

serverCron是Redis内的一个周期性函数,每隔100毫秒执行一次,它的其中一项工作就是:根据配置文件中save规则来判断当前需要进行自动持久化流程,如果满足条件则尝试开始持久化。简单了解一下这部分的运行原理。

第一次遇到这个函数,通过代码看下这个函数的代码注释。我们可以发现它会完成过期key处理、软件监控、更新一些统计数据、触发RDB持久化或AOF重写、客户端超时处理等,本节我们只关注RDB持久化部分。

/* This is our timer interrupt, called server.hz times per second.
 * Here is where we do a number of things that need to be done asynchronously.
 * For instance:
 *
 * - Active expired keys collection (it is also performed in a lazy way on
 *   lookup).
 * - Software watchdog.
 * - Update some statistic.
 * - Incremental rehashing of the DBs hash tables.
 * - Triggering BGSAVE / AOF rewrite, and handling of terminated children.
 * - Clients timeout of different kinds.
 * - Replication reconnection.
 * - Many more...
 *
 * Everything directly called here will be called server.hz times per second,
 * so in order to throttle execution of things we want to do less frequently
 * a macro is used: run_with_period(milliseconds) { .... }
 */
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {}

在redisServer中有几个与RDB持久化有关的字段(如下代码)。saveparams为配置文件中的save配置,lastsave为上次持久化的时间戳,dirty为上次持久化后发生修改的次数。

struct redisServer {
    /* 省略其他字段 */ 
    /* RDB persistence */
    long long dirty;                /* Changes to DB from the last save,上次持久化后修改key的次数 */
    struct saveparam *saveparams;   /* Save points array for RDB,对应配置文件多个save参数 */
    int saveparamslen;              /* Number of saving points,save参数的数量 */
    time_t lastsave;                /* Unix time of last successful save 上次持久化时间*/
    /* 省略其他字段 */
}

/* 对应redis.conf中的save参数 */
struct saveparam {
    time_t seconds;                    /* 统计时间范围 */   
    int changes;                    /* 数据修改次数 */
};

这部分的源码比较简单,感兴趣的同学可以下载查看,为了节省篇幅我就不贴代码了。如果没有后台的RDB持久化或AOF重写进程,serverCron会根据以上配置及状态判断是否需要执行持久化操作,判断依据就是看lastsave、dirty是否满足saveparams数组中的其中一个条件。如果有一个条件匹配,则调用rdbSaveBackground方法,执行异步持久化流程。

rdbSaveBackground

rdbSaveBackground是RDB持久化的辅助性方法,根据调用方(父进程或者子进程)不同,有两种不同的执行逻辑。如果调用方是父进程,则fork出子进程,保存子进程信息后直接返回。如果调用方是子进程则调用rdbSave执行RDB持久化逻辑,持久化完成后退出子进程。

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);

    // fork子进程
    if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) {
        int retval;

        /* Child */
        redisSetProcTitle("redis-rdb-bgsave");
        redisSetCpuAffinity(server.bgsave_cpulist);
        // 执行rdb持久化
        retval = rdbSave(filename,rsi);
        if (retval == C_OK) {
            sendChildCOWInfo(CHILD_TYPE_RDB, 1, "RDB");
        }
        // 持久化完成后,退出子进程
        exitFromChild((retval == C_OK) ? 0 : 1);
    } else {
        /* Parent 父进程:记录fork子进程的时间等信息*/
        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 */
}

rdbSave是真正执行持久化的方法,它在执行时存在大量的I/O、计算操作,耗时、CPU占用较大,在Redis的单线程模型中持久化过程会持续占用线程资源,进而导致Redis无法提供其他服务。为了解决这一问题Redis在rdbSaveBackground中fork出子进程,由子进程完成持久化工作,避免了占用父进程过多的资源。

需要注意的是,如果父进程内存占用过大,fork过程会比较耗时,在这个过程中父进程无法对外提供服务;另外,需要综合考虑计算机内存使用量,fork子进程后会占用双倍的内存资源,需要确保内存够用。通过info stats命令查看latest_fork_usec选项,可以获取最近一个fork以操作的耗时。

rdbSave

Redis的rdbSave函数是真正进行RDB持久化的函数,流程、细节贼多,整体流程可以总结为:创建并打开临时文件、Redis内存数据写入临时文件、临时文件写入磁盘、临时文件重命名为正式RDB文件、更新持久化状态信息(dirty、lastsave)。其中“Redis内存数据写入临时文件”最为核心和复杂,写入过程直接体现了RDB文件的文件格式,本着一图胜千言的理念,我按照源码流程绘制了下图。
image.png
补充说明一下,上图右下角“遍历当前数据库的键值对并写入”这个环节会根据不同类型的Redis数据类型及底层数据结构采用不同的格式写入到RDB文件中,不再展开了。我觉得大家对整个过程有个直观的理解就好,这对于我们理解Redis内部的运作机制大有裨益。

数据恢复

数据恢复是自动执行的,我们将备份文件 (例如:dump.rdb) 移动到Redis备份文件目录并启动服务即可,Redis就会自动加载文件数据至内存。Redis 服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。

这里备份文件名称及目录需要于redis.conf中的配置信息保持一致。

RDB优缺点

了解了RDB的工作原理后,对于RDB的优缺点就比较容易总结了。先来看下RDB的优点:

  • RDB是一个紧凑压缩的二进制文件,代表Redis在某一个时间点上的数据快照,非常适合用于备份、全量复制等场景。
  • RDB对灾难恢复、数据迁移非常友好,RDB文件可以转移至任何需要的地方并重新加载。
  • RDB是Redis数据的内存快照,数据恢复速度较快,相比于AOF的命令重放有着更高的性能。

事物总是有两面性的,RDB优点明显,同样也存在缺点:

  • RDB方式无法做到实时或秒级持久化。因为持久化过程是通过fork子进程后由子进程完成的,子进程的内存只是在fork操作那一时刻父进程的数据快照。而fork操作是一个耗时操作,无法做到实时性。
  • RDB持久化过程中的fork操作,会导致内存占用加倍,而且父进程数据越多,fork过程越长。
  • RDB文件有文件格式要求,不同版本的Redis会对文件格式进行调整,存在老版本无法兼容新版本的问题。

参考文献

image.png[
](https://juejin.im/post/5d8587...


码路印记
25 声望9 粉丝