Redis 是内存数据库,数据都是存储在内存中,为了不进程退出致使数据的永久丢失,须要按期将 Redis 中的数据以数据或命令的形式从内存保存到本地磁盘。当下次 Redis 重启时,利用持久化文件进行数据恢复。Redis 提供了 RDB 和 AOF 两种持久化机制,前者将当前的数据保存到磁盘,后者则是将每次执行的写命令保存到磁盘(相似于 MySQL 的 Binlog)。本文将详细介绍 RDB 和 AOF 两种持久化方案,包括操做方法和持久化的实现原理。redis
Redis 是一个基于键值对(K-V)存储的数据库服务器,下面先介绍 Redis 数据库的内部构造以及 K-V 的存储形式,有助于咱们更容易理解 Redis 的持久化机制。数据库
一个单机的 Redis 服务器默认状况下有 16 个数据库(0-15 号),数据库的个数是可配置的。Redis 默认使用的是 0 号数据库,可使用 SELECT 命令切换数据库。编程
Redis 中的每一个数据库都由一个 redis.h/redisDb 结构表示,它记录了单个 Redis 数据库的键空间、全部键的过时时间、处于阻塞状态和就绪状态的键、数据库编号等等。后端
typedef struct redisDb {
// 数据库键空间,保存着数据库中的全部键值对
dict *dict;
// 键的过时时间,字典的键为键,字典的值为过时事件 UNIX 时间戳
dict *expires;
// 正处于阻塞状态的键
dict *blocking_keys;
// 能够解除阻塞的键
dict *ready_keys;
// 正在被 WATCH 命令监视的键
dict *watched_keys;
struct evictionPoolEntry *eviction_pool;
// 数据库编号
int id;
// 数据库的键的平均 TTL,统计信息
long long avg_ttl;
} redisDb;
复制代码
因为 Redis 是一个键值对数据库(key-value pairs database), 因此它的数据库自己也是一个字典,对应的结构正是 redisDb。其中,dict 指向的是一个记录键值对数据的字典,它的键是一个字符串对象,它的值则能够是字符串、列表、哈希表、集合和有序集合在内的任意一种 Redis 类型对象。 expires 指向的是一个用于记录键的过时时间的字典,它的键为 dict 中的数据库键,它的值为这个数据库键的过时时间戳,这个值以 long long 类型表示。缓存
RDB 持久化(也称做快照持久化)是指将内存中的数据生成快照保存到磁盘里面,保存的文件后缀是 .rdb。rdb 文件是一个通过压缩的二进制文件,当 Redis 从新启动时,能够读取 rdb 快照文件恢复数据。RDB 功能最核心的是 rdbSave 和 rdbLoad 两个函数, 前者用于生成 RDB 文件并保存到磁盘,然后者则用于将 RDB 文件中的数据从新载入到内存中:安全
RDB 文件是一个单文件的全量数据,很适合数据的容灾备份与恢复,经过 RDB 文件恢复数据库耗时较短,一般 1G 的快照文件载入内存只需 20s 左右。Redis 提供了手动触发保存、自动保存间隔两种 RDB 文件的生成方式,下面先介绍 RDB 的建立和载入过程。bash
Redis 服务器默认是经过 RDB 方式完成持久化的,对应 redis.conf 文件的配置项以下:服务器
# RDB文件的名称
dbfilename dump.rdb
# 备份RDB和AOF文件存放路径
dir /usr/local/var/db/redis/
复制代码
Redis 提供了两个用于生成 RDB 文件的命令,一个是 SAVE,另外一个是 BGSAVE。而触发 Redis 进行 RDB 备份的方式有两种,一种是经过 SAVE 命令、BGSAVE 命令手动触发快照生成的方式,另外一种是配置保存时间和写入次数,由 Redis 根据条件自动触发保存操做。多线程
SAVE 是一个同步式的命令,它会阻塞 Redis 服务器进程,直到 RDB 文件建立完成为止。在服务器进程阻塞期间,服务器不能处理任何其余命令请求。架构
127.0.0.1:6379> SAVE
OK
复制代码
6266:M 15 Sep 2019 08:31:01.258 * DB saved on disk
复制代码
执行 SAVE 命令后,Redis 在服务端进程(PID 为 6266)执行了 SAVE 操做,这个操做发生期间会一直阻塞 Redis 客户端的请求处理。
BGSAVE 是一个异步式的命令,和 SAVE 命令直接阻塞服务器进程的作法不一样,BGSAVE 命令会派生出一个子进程,由子进程负责建立 RDB 文件,服务器进程(父进程)继续处理客户的命令。
127.0.0.1:6379> BGSAVE
Background saving started
复制代码
6266:M 15 Sep 2019 08:31:22.914 * Background saving started by pid 6283
6283:C 15 Sep 2019 08:31:22.915 * DB saved on disk
6266:M 15 Sep 2019 08:31:22.934 * Background saving terminated with success
复制代码
经过服务端输出的日志,能够发现 Redis 在服务端进程(PID 为 6266)会为 BGSAVE 命令单首创建(fork)一个子进程(PID 为 6283),并由子进程在后台完成 RDB 的保存过程,在操做完成以后通知父进程而后退出。在整个过程当中,服务器进程只会消耗少许时间在建立子进程和处理子进程信号量上面,其他时间都是待命状态。
BGSAVE 是触发 RDB 持久化的主流方式,下面给出 BGSAVE 命令生成快照的流程:
命令 | SAVE | BGSAVE |
---|---|---|
IO 类型 | 同步 | 异步 |
是否阻塞 | 全程阻塞 | fork 时发生阻塞 |
复杂度 | O(n) | O(n) |
优势 | 不会消耗额外的内存 | 不阻塞客户端 |
缺点 | 阻塞客户端 | fork 子进程消耗内存 |
由于 BGSAVE 命令能够在不阻塞服务器进程的状况下执行,因此 Redis 的配置文件 redis.conf 提供了一个 save 选项,让服务器每隔一段时间自动执行一次 BGSAVE 命令。用户能够经过 save 选项设置多个保存条件,只要其中任意一个条件被知足,服务器就会执行 BGSAVE 命令。 Redis 配置文件 redis.conf 默认配置了如下 3 个保存条件:
save 900 1
save 300 10
save 60 10000
复制代码
那么只要知足如下 3 个条件中的任意一个,BGSAVE 命令就会被自动执行:
好比经过命令 SET msg "hello" 插入一条键值对,等待 900 秒后 Reids 服务器进程自动触发保存,输出以下:
6266:M 15 Sep 2019 08:46:22.981 * 1 changes in 900 seconds. Saving...
6266:M 15 Sep 2019 08:46:22.986 * Background saving started by pid 6266
6476:C 15 Sep 2019 08:46:23.015 * DB saved on disk
6266:M 15 Sep 2019 08:46:23.096 * Background saving terminated with success
复制代码
Redis 服务器会周期性地操做 serverCron 函数,这个函数每隔 100 毫秒就会执行一次,它的一项任务就是检查 save 选项所设置的保存条件是否知足,若是知足的话,就自动执行 BGSAVE 命令。
和使用 SAVE 和 BGSAVE 命令建立 RDB 文件不一样,Redis 没有专门提供用于载入 RDB 文件的命令,RDB 文件的载入过程是在 Redis 服务器启动时自动完成的。启动时只要在指定目录检测到 RDB 文件的存在,Redis 就会经过 rdbLoad 函数自动载入 RDB 文件。
下面是 Redis 服务器启动时打印的日志,倒数第 2 条日志是在成功载入 RDB 文件后打印的。
$ redis-server /usr/local/etc/redis.conf
6266:M 15 Sep 2019 08:30:41.832 # Server initialized
6266:M 15 Sep 2019 08:30:41.833 * DB loaded from disk: 0.001 seconds
6266:M 15 Sep 2019 08:30:41.833 * Ready to accept connections
复制代码
因为 AOF 文件属于增量的写入命令备份,RDB 文件属于全量的数据备份,因此更新频率比 RDB 文件的更新频率高。因此若是 Redis 服务器开启了 AOF 持久化功能,那么服务器会优先使用 AOF 文件来还原数据库状态;只有在 AOF 的持久化功能处于关闭状态时,服务器才会使用使用 RDB 文件还原数据库状态。
RDB 文件是通过压缩的二进制文件,下面介绍关于 RDB 文件内部构造的一些细节。
SAVE 命令和 BGSAVE 命令都只会备份当前数据库,备份文件名默认为 dump.rdb,可经过配置文件修改备份文件名 dbfilename xxx.rdb。能够经过如下命令查看备份文件目录和 RDB 文件名称:
$ redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> CONFIG GET dir
1) "dir"
2) "/usr/local/var/db/redis"
127.0.0.1:6379> CONFIG GET dbfilename
1) "dbfilename"
2) "dump.rdb"
复制代码
RDB 文件的存储路径既能够在启动前配置,也能够经过命令动态设定。
CONFIG SET dir $newdir
CONFIG SET dbfilename $newFileName
复制代码
RDB 文件有固定的格式要求,它保存的是二进制数据,大致能够分为如下 5 部分:
一个 RDB 文件的 databases 部分包含着零个或者任意多个数据库(database),而每一个非空的 database 都包含 SELECTDB、db_number 以及 key_value_pairs 三个部分:
RDB 的 key_value_pairs 部分保存了一个或者多个键值对,若是键值对有过时时间,过时时间会被保存在键值对的前面。下面是这两种键值对的内部结构:
为了查看 RDB 文件内部的结构,执行如下命令往 Redis 服务器插入 3 条键值对数据:
127.0.0.1:6379> SADD fruits "apple" "banana" "orange"
(integer) 3
127.0.0.1:6379> LPUSH numbers 128 256 512
(integer) 3
127.0.0.1:6379> SET msg "hello"
OK
复制代码
执行 SAVE 操做,将 Redis 进程中的数据强制持久化到 dump.rdb 文件中
127.0.0.1:6379> SAVE
OK
复制代码
经过 Linux 的 od 命令将二进制文件 dump.rdb 中的数据转换为 ASCII 格式输出,跟前面提到的存储格式大体是同样的:
$ od -c dump.rdb
0000000 R E D I S 0 0 0 9 372 \t r e d i s
0000020 - v e r 005 5 . 0 . 5 372 \n r e d i
0000040 s - b i t s 300 @ 372 005 c t i m e 200
0000060 200 200 231 ] 372 \b u s e d - m e m 302 200
0000100 \v 020 \0 372 \f a o f - p r e a m b l
0000120 e 300 \0 376 \0 373 003 \0 \0 003 m s g 005 h e
0000140 l l o 016 \a n u m b e r s 001 027 027 \0
0000160 \0 \0 022 \0 \0 \0 003 \0 \0 300 \0 002 004 300 \0 001
0000200 004 300 200 \0 377 002 006 f r u i t s 003 006 o
0000220 r a n g e 005 a p p l e 006 b a n a
0000240 n a 377 214 ک ** 3 366 < r X
0000253
复制代码
下面是 redis.conf 文件中和 RDB 文件相关的经常使用配置项(以及默认值):
RDB 持久化是按期把内存中的数据全量写入到文件中,除此以外,RDB 还提供了基于 AOF(Append Only File)的持久化功能。AOF 会把 Redis 服务器每次执行的写命令记录到一个日志文件中,当服务器重启时再次执行 AOF 文件中的命令来恢复数据。
AOF 的主要做用是解决了数据持久化的实时性,目前已经成为了 Redis 持久化的主流方式。
默认状况下 AOF 功能是关闭的,Redis 只会经过 RDB 完成数据持久化的。开启 AOF 功能须要 redis.conf 文件中将 appendonly 配置项修改成 yes,这样在开启 AOF 持久化功能的同时,将基于 RDB 的快照持久化置于低优先级。修改 redis.conf 以下:
# 此选项为AOF功能的开关,默认为no,经过yes来开启aof功能
appendonly yes
# 指定AOF文件名称
appendfilename appendonly.aof
# 备份RDB和AOF文件存放路径
dir /usr/local/var/db/redis/
复制代码
重启 Redis 服务器进程之后,dir 目录下会生成一个 appendonly.aof 文件,因为此时服务器未执行任何写指令,所以 AOF 文件是空的。执行如下命令写入几条测试数据:
127.0.0.1:6379> SADD fruits "apple" "banana" "orange"
(integer) 3
127.0.0.1:6379> LPUSH numbers 128 256 512
(integer) 3
127.0.0.1:6379> SET msg "hello"
OK
复制代码
AOF 文件是纯文本格式的,上述写命令按顺序被写入了 appendonly.aof 文件(省掉换行符 '\r\n'):
/usr/local/var/db/redis$ cat appendonly.aof
*2 $6 SELECT $1 0
*5 $4 SADD $6 fruits $5 apple $6 banana $6 orange
*5 $5 LPUSH $7 numbers $3 128 $3 256 $3 512
*3 $3 SET $3 msg $5 hello
复制代码
RDB 持久化的方式是将 apple、banana、orange 的键值对数据保存为 RDB 的二进制文件,而 AOF 是经过把 Redis 服务器执行的 SADD、LPUSH、SET 等命令保存到 AOF 的文本文件中。下图是 AOF 文件内部的构造图:
再次重启 Redis 服务器进程,观察启动日志会发现 Redis 会经过 AOF 文件加载数据:
52580:M 15 Sep 2019 16:09:47.015 # Server initialized
52580:M 15 Sep 2019 16:09:47.015 * DB loaded from append only file: 0.001 seconds
52580:M 15 Sep 2019 16:09:47.015 * Ready to accept connections
复制代码
经过命令读取 AOF 文件还原的键值对数据:
127.0.0.1:6379> SMEMBERS fruits
1) "apple"
2) "orange"
3) "banana"
127.0.0.1:6379> LRANGE numbers 0 -1
1) "512"
2) "256"
3) "128"
127.0.0.1:6379> GET msg
"hello"
复制代码
AOF 不须要设置任何触发条件,对 Redis 服务器的全部写命令都会自动记录到 AOF 文件中,下面介绍 AOF 持久化的执行流程。
AOF 文件的写入流程能够分为如下 3 个步骤:
Redis 使用单线程处理客户端命令,为了不每次有写命令就直接写入磁盘,致使磁盘 IO 成为 Redis 的性能瓶颈,Redis 会先把执行的写命令追加(append)到一个 aof_buf 缓冲区,而不是直接写入文件。
命令追加的格式是 Redis 命令请求的协议格式,它是一种纯文本格式,具备兼容性好、可读性强、容易处理、操做简单避免二次开销等优势。在 AOF 文件中,除了用于指定数据库的 select 命令(好比:select 0 为选中 0 号数据库)是由 Redis 添加的,其余都是客户端发送来的写命令。
Redis 提供了多种 AOF 缓存区的文件同步策略,相关策略涉及到操做系统的 write() 函数和 fsync() 函数,说明以下:
为了提升文件的写入效率,当用户调用 write 函数将数据写入文件时,操做系统会先把数据写入到一个内存缓冲区里,当缓冲区被填满或超过了指定时限后,才真正将缓冲区的数据写入到磁盘里。
虽然操做系统底层对 write() 函数进行了优化 ,但也带来了安全问题。若是宕机内存缓冲区中的数据会丢失,所以系统同时提供了同步函数 fsync() ,强制操做系统马上将缓冲区中的数据写入到磁盘中,从而保证了数据持久化。
Redis 提供了 appendfsync 配置项来控制 AOF 缓存区的文件同步策略,appendfsync 可配置如下三种策略:
命令写入 aof_buf 缓冲区后当即调用系统 fsync 函数同步到 AOF 文件,fsync 操做完成后线程返回,整个过程是阻塞的。这种状况下,每次有写命令都要同步到 AOF 文件,硬盘 IO 成为性能瓶颈,Redis 只能支持大约几百 TPS 写入,严重下降了 Redis 的性能。
命令写入 aof_buf 缓冲区后调用系统 write 操做,不对 AOF 文件作 fsync 同步;同步由操做系统负责,一般同步周期为 30 秒。这种状况下,文件同步的时间不可控,且缓冲区中堆积的数据会不少,数据安全性没法保证。
命令写入 aof_buf 缓冲区后调用系统 write 操做,write 完成后线程马上返回,fsync 同步文件操做由单独的进程每秒调用一次。everysec 是前述两种策略的折中,是性能和数据安全性的平衡,所以也是 Redis 的默认配置,也是比较推崇的配置选项。
文件同步策略 | write 阻塞 | fsync 阻塞 | 宕机时的数据丢失量 |
---|---|---|---|
always | 阻塞 | 阻塞 | 最多只丢失一个命令的数据 |
no | 阻塞 | 不阻塞 | 操做系统最后一次对 AOF 文件 fsync 后的数据 |
everysec | 阻塞 | 不阻塞 | 通常不超过 1 秒钟的数据 |
随着命令不断写入 AOF,文件会愈来愈大,致使文件占用空间变大,数据恢复时间变长。为了解决这个问题,Redis 引入了重写机制来对 AOF 文件中的写命令进行合并,进一步压缩文件体积。
AOF 文件重写指的是把 Redis 进程内的数据转化为写命令,同步到新的 AOF 文件中,而后使用新的 AOF 文件覆盖旧的 AOF 文件,这个过程不对旧的 AOF 文件的进行任何读写操做。
AOF 重写过程提供了手动触发和自动触发两种机制:
下面以手动触发 AOF 重写为例,当 bgrewriteaof 命令被执行时,AOF 文件重写的流程以下:
文件重写之因此可以压缩 AOF 文件的大小,缘由在于如下几方面:
下面是 redis.conf 文件中和 AOF 文件相关的经常使用配置项(以及默认值):
前面提到当 AOF 持久化功能开启时,Redis 服务器启动时优先执行 AOF 文件的命令恢复数据,只有当 AOF 功能关闭时,才会优先载入 RDB 快照的文件数据。
6266:M 15 Sep 2019 08:30:41.832 # Server initialized
6266:M 15 Sep 2019 08:30:41.833 * DB loaded from disk: 0.001 seconds
6266:M 15 Sep 2019 08:30:41.833 * Ready to accept connections
复制代码
9447:M 15 Sep 2019 23:01:46.601 # Server initialized
9447:M 15 Sep 2019 23:01:46.602 * DB loaded from append only file: 0.001 seconds
9447:M 15 Sep 2019 23:01:46.602 * Ready to accept connections
复制代码
9326:M 15 Sep 2019 22:49:24.203 # Server initialized
9326:M 15 Sep 2019 22:49:24.203 * Ready to accept connections
复制代码
持久化机制 | RDB | AOF |
---|---|---|
启动优先级 | 低 | 高 |
磁盘文件体积 | 小 | 大 |
数据还原速度 | 快 | 慢 |
数据安全性 | 容易丢失数据 | 根据策略决定 |
操做轻重级别 | 重 | 轻 |
在重启 Redis 服务器时,通常不多使用 RDB 快照文件来恢复内存状态,由于会丢失大量数据。更多的是使用 AOF 文件进行命令重放,可是执行 AOF 命令性能相对 RDB 来讲要慢不少。这样在 Redis 数据很大的状况下,启动须要消耗大量的时间。
鉴于 RDB 快照可能会形成数据丢失,AOF 指令恢复数据慢,Redis 4.0 版本提供了一套基于 AOF-RDB 的混合持久化机制,保留了两种持久化机制的优势。这样重写的 AOF 文件由两部份组成,一部分是 RDB 格式的头部数据,另外一部分是 AOF 格式的尾部指令。
Redis 4.0 版本的混合持久化功能默认是关闭的,经过配置 aof-use-rdb-preamble 为 yes 开启此功能:
# 开启AOF-RDB混合持久化机制
aof-use-rdb-preamble yes
复制代码
查看 Redis 服务器是否开启混合持久化功能:
127.0.0.1:6379> CONFIG GET aof-use-rdb-preamble
1) "aof-use-rdb-preamble"
2) "yes"
复制代码
如图所示,将 RDB 数据文件的内容和增量的 AOF 命令文件存在一块儿。这里的 AOF 命令再也不是全量的命令,而是自持久化开始到持久化结束的这段时间服务器进程执行的增量 AOF 命令,一般这部分 AOF 命令很小。
在 Redis 服务器重启的时候,能够预先加载 AOF 文件头部全量的 RDB 数据,而后再重放 AOF 文件尾部增量的 AOF 命令,从而大大减小了重启过程当中数据还原的时间。
在介绍持久化策略以前,首先要明白不管是 RDB 仍是 AOF 方式,开启持久化都是会形成性能开销的。
相对来讲,因为 AOF 向磁盘中写入数据的频率更高,所以对 Redis 服务器主进程性能的影响会更大。
在实际生产环境中,根据数据量、应用对数据的安全要求、预算限制等不一样状况,会有各类各样的持久化策略。
对于分布式环境,持久化的选择必须与 Redis 的主从策略一块儿考虑,由于主从复制与持久化一样具备数据备份的功能,并且主节点(Master Node)和从节点(Slave Node)能够独立选择持久化方案。
下面分场景来讨论持久化策略的选择,下面的讨论也只是做为参考,实际方案可能更复杂更具多样性。
若是 Redis 中的数据彻底丢弃也没有关系(如 Redis 彻底用做 DB 层数据的缓存),那么不管是单机,仍是主从架构,均可以不进行任何持久化。
在单机环境下,若是能够接受十几分钟或更多的数据丢失,RDB 方案对 Redis 的性能更加有利;若是只能接受秒级别的数据丢失,选择 AOF 方案更合适。
在多数状况下,Redis 都会配置主从部署机制。从节点(slave)既能够实现数据的热备,也能够进行读写分担 Redis 读请求,以及在主节点(master)宕机后的顶替做用。
在这种状况下,一种可行的作法以下:
为何开启了主从复制,能够实现数据的热备份,还须要设置持久化呢?由于在一些特殊状况下,主从复制仍然不足以保证数据的安全,例如:
前面的几种持久化策略,针对的都是通常的系统故障,如进程异常退出、宕机、断电等,这些故障不会损坏硬盘。可是对于一些可能致使硬盘损坏的灾难状况,如火灾地震,就须要进行异地灾备。
因为 RDB 文件文件小、恢复速度快,灾难恢复通常采用 RDB 方式;异地备份的频率根据数据安全性的须要及其它条件来肯定,但最好不要低于一天一次。
本文主要开篇介绍了 Redis 服务器的数据库结构,进一步介绍了 Redis 提供的几种持久化机制,包括基于数据快照的 RDB 全量持久化、基于命令追加的 AOF 增量持久化以及 Redis 4.0 支持的混合持久化。对于 RDB 的持久化方式,给出了 RDB 快照的建立和还原过程,RDB 的文件结构以及相关配置项。对于 AOF 的持久化方式,给出了 AOF 日志的建立和还原过程,AOF 的执行流程,AOF 文件内部的格式以及相关配置项。在文章结尾分析了 RDB 和 AOF 方式各自的优缺点,性能开销,以及在单机环境、主从部署、异地备灾场景下的持久化策略。
本账号持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。