Redis持久化(RDB&AOF)

344 阅读13分钟

介绍

Redis是一个键值对数据库服务器,服务器中包含着任意个非空数据库,每个非空数据库又可以包含任意个键值对。如下图所示:

redis数据库状态示例 (1).png

Redis对外提供数据访问服务时使用的是驻存在内存中的数据,这些数据在Redis重启之后将消失。为了让数据在重启之后得以恢复,Redis具备将数据持久化到本地磁盘的能力。

Redis的持久化有三种方式:全量模式(RDB)、增量模式(AOF) 和 混合持久化。

全量模式持久化(RDB)

Redis作为一个有状态的节点,其“状态”可以用实例内部所有db的key-value值来定义,每次Redis处理一个数据访问写命令修改了db的key-value数据时,Redis就发生了一次状态变迁。基于全量的持久化即在持久化出触发的时刻,将当时的状态(所有db的key-value值)完全保存下来,形成一个snapshot(RDB文件)。如下图所示:

全量模式持久化(RDB).png

RDB文件是一个经过压缩的二进制文件,通过该RDB文件可以还原生成RDB文件时的数据库状态。

RDB文件默认文件名为dump.rdb

# The filename where to dump the DB
dbfilename dump.rdb

RDB持久化写入流程

Redis全量写入包含两种方式(两个Redis命令):SAVEBGSVAVE

SAVE

SAVE可以由客户端显式触发,也可以在redis shutdown时触发,无论哪种方式,SAVE都是以单线程串行化的执行命令。SAVE命令会阻塞服务器进程,服务器进程阻塞期间不能处理其它命令请求,所以SAVE方式可以保证此刻数据状态是一致的,不会发生变更。如果数据量比较大的话,持久化持续时间会比较长。

redis save.png

BGSAVE

BGSAVE可以由客户端通过命令显式触发,可以通过配置由定时任务触发,也可以在master-slave的分布式结构下由slave节点触发。

BGSAVE命令执行会fork一个子进程,然后由子进程负责创建RDB文件,服务进程继续处理命令请求(fork子进程异步持久化)。子进程中会复制保存一份父进程在fork时的数据库状态副本(复制fork子进程时父进程的内存)。子进程数据库状态副本后期不会发生变更,子进程会把该副本并发写入到RDB文件。

redis bgsave.png

前面讲到BGSAVE可以通过配置(设置服务器配置的save选项)由定时任务触发,那是因为BGSAVE是在不阻塞服务器进程情况下执行的,所以我们可以通过save选项配置来让服务器每隔一段时间自动执行一次BGSAVE命令。

Redis服务器默认的save选项配置如下:

#
#   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

满足以上任意一个条件都会自动进行BGSAVE命令进行备份。例如save 900 1表示服务器在900s之内对数据库进行了至少一次修改。我们可以根据自己需要进行自定义配置。

SAVE 和 BGSAVE的对比

redis save和bgsave对比 (1).png

BGSAVE相比于SAVE的优势是持久化期间可以持续提供数据读写服务,但子线程fork时涉及父进程内存的复制,其存在期间会增加服务器内存的开销,当内存开销高到使用虚拟内存时,BGSAVE的fork会阻塞服务器运行,造成秒级以上的不可用。因此使用BGSAVE需保证足够的空闲内存。

RDB持久化恢复流程

Redis启动会从本地磁盘加载之前持久化的文件到内存,然后再处理后续来自客户端的数据访问命令。持久化方法loadDataFromDisk方法来自于server.c。

/* Function called at startup to load RDB or AOF file in memory. */
void loadDataFromDisk(void) {
    long long start = ustime();
    //服务器开启了AOF持久化则优先使用AOF文件来还原数据库状态
    if (server.aof_state == AOF_ON) {
        //loadAppendOnlyFile即为加载AOF文件到内存的方法
        if (loadAppendOnlyFile(server.aof_filename) == C_OK)
            serverLog(LL_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
    } else {
        rdbSaveInfo rsi = RDB_SAVE_INFO_INIT;
        //AOF持久化未开启则使用RDB文件还原数据库状态(rdbLoad方法)
        if (rdbLoad(server.rdb_filename,&rsi) == C_OK) {
            serverLog(LL_NOTICE,"DB loaded from disk: %.3f seconds",
                (float)(ustime()-start)/1000000);

            /* Restore the replication ID / offset from the RDB file. */
            if (server.masterhost &&
                rsi.repl_id_is_set &&
                rsi.repl_offset != -1 &&
                /* Note that older implementations may save a repl_stream_db
                 * of -1 inside the RDB file in a wrong way, see more information
                 * in function rdbPopulateSaveInfo. */
                rsi.repl_stream_db != -1)
            {
                memcpy(server.replid,rsi.repl_id,sizeof(server.replid));
                server.master_repl_offset = rsi.repl_offset;
                /* If we are a slave, create a cached master from this
                 * information, in order to allow partial resynchronizations
                 * with masters. */
                replicationCacheMasterUsingMyself();
                selectDb(server.cached_master,rsi.repl_stream_db);
            }
        } else if (errno != ENOENT) {
            serverLog(LL_WARNING,"Fatal error loading the DB: %s. Exiting.",strerror(errno));
            exit(1);
        }
    }
}

我们可以发现RDB持久化由于是全量备份,所以备份的频率不能过于频繁,那么在突然宕机的时候丢失的数据较多,当前时间和上次生成快照时间段内的数据不会被备份到。

既然全量持久化宕机时候会丢失较多数据,那么是不是提供了其它持久化方式遇到特殊情况减少丢失的数据那。有的,那就是接下来要介绍的增量模式持久化,它RDB持久化有很大不同,RDB持久化保存的快照数据是Redis保存的key-value键值对数据,而增量模式持久化是通过保存写命令来记录数据库状态

增量模式持久化(AOF)

AOF持久化需要手动设置redis.conf中的appendonly为yes,默认是no不开启的。

# By default Redis asynchronously dumps the dataset on disk. This mode is
# good enough in many applications, but an issue with the Redis process or
# a power outage may result into a few minutes of writes lost (depending on
# the configured save points).
#
# The Append Only File is an alternative persistence mode that provides
# much better durability. For instance using the default data fsync policy
# (see later in the config file) Redis can lose just one second of writes in a
# dramatic event like a server power outage, or a single write if something
# wrong with the Redis process itself happens, but the operating system is
# still running correctly.
#
# AOF and RDB persistence can be enabled at the same time without problems.
# If the AOF is enabled on startup Redis will load the AOF, that is the file
# with the better durability guarantees.
#
# Please check http://redis.io/topics/persistence for more information.

appendonly no

AOF(append-only-file)持久化是通过追加保存Redis服务器所执行的写命令来记录数据库状态的。若开启了AOF持久化功能,当Redis启动的时候通过执行AOF文件中的写命令来恢复内存中的数据。

redis aof.png

AOF持久化写入流程

AOF持久化实现可分为命令追加、文件写入、文件同步三个步骤。我们来依次分析下。

命令追加

当AOF持久化功能处于打开状态,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾(append到redisServer对象的aof_buf变量中)。

AOF文件的写入和同步

Redis服务器进程是一个事件循环,循环中文件事件负责接收客户端请求和向客户端发送命令回复。时间事件负责执行定时运行的函数。文件事件可能执行写命令,一些内容追加到aof_buf缓冲区中。主循环在下一个迭代进入多路复用的select方法前,会调用flushAppendOnlyFile方法将aof_buf的内容write到AOF对应的文件中。但write操作只是将数据写到缓存中,什么时候真正落到磁盘上取决于操作系统。只有显式调用fsync()方法才能让操作系统落地数据到磁盘

AOF持久化写入.png

flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的设置来决定.

# The fsync() call tells the Operating System to actually write data on disk
# instead of waiting for more data in the output buffer. Some OS will really flush
# data on disk, some other OS will just try to do it ASAP.
#
# Redis supports three different modes:
#
# no: don't fsync, just let the OS flush the data when it wants. Faster.
# always: fsync after every write to the append only log. Slow, Safest.
# everysec: fsync only one time every second. Compromise.
#
# The default is "everysec", as that's usually the right compromise between
# speed and data safety. It's up to you to understand if you can relax this to
# "no" that will let the operating system flush the output buffer when
# it wants, for better performances (but if you can live with the idea of
# some data loss consider the default persistence mode that's snapshotting),
# or on the contrary, use "always" that's very slow but a bit safer than
# everysec.
#
# More details please check the following article:
# http://antirez.com/post/redis-persistence-demystified.html
#
# If unsure, use "everysec".

# appendfsync always
appendfsync everysec
# appendfsync no

AOF的三种同步策略就是appendfsync选项来配置的。

aof 三种同步策略.png

默认生成的AOF文件名称为appendonly.aof

# The name of the append only file (default: "appendonly.aof")

appendfilename "appendonly.aof"

AOF持久化恢复流程

也是在Redis启动的时候如果存在AOF则使用AOF文件进行恢复内存数据。过程就是将AOF文件中的命令重新执行一遍。相对RDB文件,AOF文件数据更全。所以优先选择AOF文件进行恢复数据。

我们可以看到AOF持久化是通过保存被执行的写命令记录数据库状态。AOF文件体积会越来越大,可能会对Redis服务器造成影响,同时AOF进行数据还原可能会比较耗时。Redis提供了什么方法进行优化吗?有的,Redis提供了文件重写(rewrite)来进行缩小AOF文件体积。

AOF重写

我们举例来看一下重写前后的AOF文件变化:

aof重写优化前和优化后 (1).png

从上图我们可以看出AOF文件重写以后清理了过期数据的写命令并对相同key的数据用当前最新数据新增的一条命令来代替(1条新增命令替代多条历史命令)。

我们可以总结下AOF重写的原理:首先从数据库中读取键现在的值,然后用一条命令去记录键值对,替代之前记录这个键值对的多条命令。

AOF重写程序aof_rewrite函数由主进程调用的话,则在重写期间无法处理客户端发来的命令请求。由于涉及到大量的写操作,这是不可取的。所以Redis是将AOF重写程序放到子线程执行。后台AOF重写和bgrewriteaof命令的实现原理是一样的。

那么在什么条件下会触发AOF重写?这个在redis.conf配置的参数中定义了默认的配置(当然可根据需要修改):

# Automatic rewrite of the append only file.
# Redis is able to automatically rewrite the log file implicitly calling
# BGREWRITEAOF when the AOF log size grows by the specified percentage.
#
# This is how it works: Redis remembers the size of the AOF file after the
# latest rewrite (if no rewrite has happened since the restart, the size of
# the AOF at startup is used).
#
# This base size is compared to the current size. If the current size is
# bigger than the specified percentage, the rewrite is triggered. Also
# you need to specify a minimal size for the AOF file to be rewritten, this
# is useful to avoid rewriting the AOF file even if the percentage increase
# is reached but it is still pretty small.
#
# Specify a percentage of zero in order to disable the automatic AOF
# rewrite feature.

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

auto-aof-rewrite-min-size 配置表示aof文件进行重写的最小容量,达不到则不进行重写。为了减少重写次数,该配置可以配置相对较大一些,个人建议配置6G以上

auto-aof-rewrite-percentage配置表示超过AOF优化后文件大小多少百分比时进行重写。100表示达到AOF优化后文件两倍大小进行重写(前提是满足大于auto-aof-rewrite-min-size的条件)。

如下图所示AOF模式的rewrite机制的写入流程图:

AOF持久化rewrite.png

根据上图我们来理下AOF重写相关流程:

1)主循环运行到定时任务判断Rewrite条件是否满足,满足的话则通过rewriteAppendOnlyFileBackground函数fork出一个子进程,子进程创建完成后获得了主进程当时的数据状态(和旧的AOF文件无关,直接读取内存中数据状态)

2)子进程将状态写入rewrite的AOF文件中。子线程运行期间,Redis主线程继续对外提供服务,新的增量写入redisServer对象的aof_rewrite_buf_blocks中

3)子线程完成后,会向父进程发送一个信号,父进程接收到该信号以后,会调用一个信号处理函数

4)信号处理函数会将AOF重写缓冲区的所有内容append到rewrite快照文件末尾,然后对rewrite快照文件改名原子覆盖现有的AOF文件。服务器进程执行该步骤操作会阻塞,所以这时新的AOF文件保存数据和服务器当前数据是一致的。

5)上面步骤完成后,后面的写命令就和正常一样写入到新的AOF文件

RDB持久化和AOF持久化的对比

通过以上分析,RDB恢复数据比较快,保存的就是数据本身,适合大规模数据恢复和冷备。由于RDB为全量持久化,执行频率比AOF低,则AOF相对RDB来说在宕机的时候丢失的数据更少,而且AOF采用的是增量追加的模式,写入性能会比较高。但AOF不适合冷备,恢复的文件可能会比较大,恢复的速度慢,已执行命令的方式恢复,可能会出现恢复不稳定的情况。

混合持久化(AOF+RDB)

redis4.0开始支持混合持久化,如果需要支持混合持久化,则需要打开下面的配置(默认为打开):

# 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-use-rdb-preamble配置打开以后,重写的AOF文件则会被优化为前半段为RDB数据,后半段为AOF数据([RDB file][AOF tail])。当加载 Redis 时识别到 AOF 文件以“REDIS”字符串开头并加载带前缀的 RDB 文件,加载完RDB再继续加载后面的AOF。

混合持久化也是根据bgrewriteaof来完成的,不同的是在fork出的子进程会将内存副本全量的以RDB方式写入aof文件,然后将重写缓冲区的aof_rewrite_buf_blocks内容追加到AOF文件后面。其它流程和AOF重写是一样的。

这样做有什么好处?

  • 重写和恢复加载速度会提高(后期AOF文件中的RDB数据会占很大一部分)
  • AOF文件后面都是以增量模式添加写命令,会减少在宕机等特殊情况下数据的丢失

当然使用混合持久化模式也有不好的地方,那就是AOF文件的内容形式就不那么统一了,AOF文件就会变得稍微复杂一些。

实际运用RDB和AOF怎么选择

RDB和AOF各有千秋,不建议只使用AOF持久化的方式,因为RDB保存的数据而不是写命令,更有利于数据备份。另外RDB恢复数据的速度更快。AOF虽然数据更全,增量追加的速度更快,但是大数据量情况下,恢复数据的速度很慢(执行写命令)。

生产环境建议RDB和AOF同时使用,使用RDB作为容灾备份。如果你的redis版本大于等于4.0则建议使用混合持久化

参考书籍:《Redis设计与实现》《深入分布式缓存》