Redis 是一种内存数据库,将数据保存在内存中,读写效率要比传统的将数据保存在磁盘上的数据库要快很多。但是一旦进程退出,Redis 的数据就会丢失。
为了解决这个问题,Redis 提供了 RDB 和 AOF 两种持久化方案,将内存中的数据保存到磁盘中,避免数据丢失。
antirez 在《Redis 持久化解密》一文中说,一般来说有三种常见的策略来进行持久化操作,防止数据损坏:
- 方法1 是数据库不关心发生故障,在数据文件损坏后通过数据备份或者快照来进行恢复。Redis 的 RDB 持久化就是这种方式。
- 方法2 是数据库使用操作日志,每次操作时记录操作行为,以便在故障后通过日志恢复到一致性的状态。因为操作日志是顺序追加的方式写的,所以不会出现操作日志也无法恢复的情况。类似于 Mysql 的 redo 和 undo 日志
- 方法3 是数据库不进行老数据的修改,只是以追加方式去完成写操作,这样数据本身就是一份日志,这样就永远不会出现数据无法恢复的情况了。CouchDB就是此做法的优秀范例。
RDB 就是第一种方法,它就是把当前 Redis 进程的数据生成时间点快照( point-in-time snapshot ) 保存到存储设备的过程。
在 Redis 运行时, RDB 程序将当前内存中的数据库快照保存到磁盘文件中, 在 Redis 重启动时, RDB 程序可以通过载入 RDB 文件来还原数据库的状态。
RDB 功能最核心的是 rdbSave
和 rdbLoad
两个函数, 前者用于生成 RDB 文件到磁盘, 而后者则用于将 RDB 文件中的数据重新载入到内存中
每次快照持久化都是将内存数据完整写入到磁盘一次,并不 是增量的只同步脏数据。如果数据量大的话,而且写操作比较多,必然会引起大量的磁盘io操作,可能会严重影响性能
1. 触发机制
RDB 触发机制分为使用指令手动触发和 redis.conf 配置自动触发。
1.1. 手动触发
save命令
阻塞当前redis服务器,直到RDB过程完成为止。 只有在上一个 SAVE 执行完毕、 Redis 重新开始接受请求之后, 新的 SAVE 、 BGSAVE 或 BGREWRITEAOF 命令才会被处理。
执行save命令时对应的redis日志如下:
13737:M 20 Nov 14:18:39.398 * DB saved on disk
bgsave命令
Redis 会在后台异步执行快照操作,此时 Redis 仍然可以相应客户端请求。具体操作是 Redis 进程执行 fork
操作创建子进程,RDB 持久化过程由子进程负责,完成后自动结束。Redis 只会在 fork
期间发生阻塞,但是一般时间都很短。但是如果 Redis 数据量特别大, fork
时间就会变长,而且占用内存会加倍,这一点需要特别注意。
执行bgsave命令时,日志如下
13737:M 20 Nov 14:20:01.823 * Background saving started by pid 13756
13756:C 20 Nov 14:20:01.827 * DB saved on disk
13756:C 20 Nov 14:20:01.827 * RDB: 6 MB of memory used by copy-on-write
13737:M 20 Nov 14:20:01.927 * Background saving terminated with success
在执行 SAVE 命令之前, 服务器会检查 BGSAVE 是否正在执行当中, 如果是的话, 服务器就不调用 rdbSave , 而是向客户端返回一个出错信息, 告知在 BGSAVE 执行期间, 不能执行 SAVE 。 这样做可以避免 SAVE 和 BGSAVE 调用的两个 rdbSave 交叉执行, 造成竞争条件。 另一方面, 当 BGSAVE 正在执行时, 调用新 BGSAVE 命令的客户端会收到一个出错信息, 告知 BGSAVE 已经在执行当中。
BGREWRITEAOF
和 BGSAVE
不能同时执行:
- 如果 BGSAVE 正在执行,那么 BGREWRITEAOF 的重写请求会被延迟到 BGSAVE 执行完毕之后进行,执行 BGREWRITEAOF 命令的客户端会收到请求被延迟的回复。
- 如果 BGREWRITEAOF 正在执行,那么调用 BGSAVE 的客户端将收到出错信息,表示这两个命令不能同时执行。
BGREWRITEAOF
和 BGSAVE
两个命令在操作方面并没有什么冲突的地方, 不能同时执行它们只是一个性能方面的考虑: 并发出两个子进程, 并且两个子进程都同时进行大量的磁盘写入操作, 这怎么想都不会是一个好主意。
1.2. 自动触发
- 通过save相关配置,可以实现RDB的自动持久化机制(bgsave)
save 900 1 #在900s(15m)之后,至少有1个key发生变化 save 300 10 #在300s(5m)之后,至少有10个key发生变化 save 60 10000 #在60s(1m)之后,至少有1000个key发生变化
不设任何值
save ""
即为关闭 - 如果从节点执行全量复制操作,主节点自动执行bgsave生产RDB文件并发送给从节点
-
执行debug reload命令重新加载redis时,也会自动触发save操作
13802:M 20 Nov 14:33:58.435 * DB saved on disk 13802:M 20 Nov 14:33:58.435 # DB reloaded by DEBUG RELOAD
-
默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave
13737:M 20 Nov 14:27:24.428 # User requested shutdown… 13737:M 20 Nov 14:27:24.428 * Saving the final RDB snapshot before exiting. 13737:M 20 Nov 14:27:24.431 * DB saved on disk 13737:M 20 Nov 14:27:24.431 # Redis is now ready to exit, bye bye…
save m n的实现原理
Redis的save m n
,是通过serverCron函数、dirty计数器、和lastsave时间戳来实现的
serverCron是Redis服务器的周期性操作函数,默认每隔100ms执行一次;该函数对服务器的状态进行维护,其中一项工作就是检查save选项所设置的保存条件是否满足,如果满足就执行bgsave命令。检查的过程:根据系统当前时间、dirty和lastsave属性的值来检验保存条件是否满足。
服务器内部维护了一个dirty计数器和lastsave属性:
- dirty:记录了距上次成功执行了save或bgsave命令之后,服务器状态进行了多少次修改(包括增删改);而当save/bgsave执行完成后,会将dirty重新置为0。例如,如果Redis执行了set mykey helloworld,则dirty值会+1;如果执行了sadd myset v1 v2 v3,则dirty值会+3;注意dirty记录的是服务器进行了多少次修改,而不是客户端执行了多少修改数据的命令。
- lastsave:unix时间戳,记录了上一次成功执行save或bgsave命令的时间;
save m n的原理如下:每隔100ms,执行serverCron函数;在serverCron函数中,遍历save m n配置的保存条件,只要有一个条件满足,就进行bgsave。对于每一个save m n条件,只有下面两条同时满足时才算满足:
- 当前时间-lastsave > m
- dirty >= n
2. bgsave的执行流程
下图说明了bgsave命令的执行流程:
- Redis父进程首先判断:当前是否在执行save,或bgsave/bgrewriteaof(后面会详细介绍该命令)的子进程,如果在执行则bgsave命令直接返回。bgsave/bgrewriteaof 的子进程不能同时执行,主要是基于性能方面的考虑:两个并发的子进程同时执行大量的磁盘写操作,可能引起严重的性能问题。
- 父进程执行fork操作创建子进程,这个过程中父进程是阻塞的,Redis不能执行来自客户端的任何命令;
- 父进程fork后,bgsave命令返回”Background saving started”信息并不再阻塞父进程,并可以响应其他命令;
- 子进程创建RDB文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换;
- 子进程发送信号给父进程表示完成,父进程更新统计信息。
通过info stats
命令查看latest_fork_usec
选项,可以获取最近一个fork操作的耗时,单位是微秒。
通过info Persistence
命令可以查看RDB相关选项,具体含义查看info命令
127.0.0.1:6379> info Persistence
# Persistence
loading:0
rdb_changes_since_last_save:0
rdb_bgsave_in_progress:0
rdb_last_save_time:1511159638
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:-1
rdb_current_bgsave_time_sec:-1
rdb_last_cow_size:0
aof_enabled:0
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:-1
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
aof_last_cow_size:0
相关参数
# 持久化 rdb文件遇到问题时,主进程是否接受写入,yes 表示停止写入,如果是no 表示redis继续提供服务。
stop-writes-on-bgsave-error yes
# 在进行快照镜像时,是否进行压缩。yes:压缩,但是需要一些cpu的消耗。no:不压缩,需要更多的磁盘空间。
rdbcompression yes
# 一个CRC64的校验就被放在了文件末尾,当存储或者加载rbd文件的时候会有一个10%左右的性能下降,为了达到性能的最大化,你可以关掉这个配置项。
rdbchecksum yes
# 快照的文件名
dbfilename dump.rdb
# 存放快照的目录
dir /var/lib/redis
RDB默认使用LZF算法对生成的RDB进行压缩处理,压缩后的内存远小于内存大小。 redis-check-dump工具可以检测RDB文件兵获取对应的错误报告
优点
RDB文件小,非常适合定时备份,用于灾难恢复。
因为RDB文件中直接存储的是内存数据,而AOF文件中存储的是一条条命令,需要应用命令。Redis加载RDB文件的速度比AOF快很多。
缺点
RDB持久化方式不能做到实时/秒级持久化。实时持久化要全量刷内存到磁盘,成本太高。每秒fork子进程也会阻塞主进程,影响性能。
RDB文件是二进制文件,随着Redis不断迭代有多个rdb文件的版本,不支持跨版本兼容。老的Redis无法识别新的RDB文件格式
3. RDB文件结构
一个 RDB 文件可以分为以下几个部分:
+-------+-------------+-----------+-----------------+-----+-----------+
| REDIS | RDB-VERSION | SELECT-DB | KEY-VALUE-PAIRS | EOF | CHECK-SUM |
+-------+-------------+-----------+-----------------+-----+-----------+
|<-------- DB-DATA ---------->|
REDIS
文件的最开头保存着 REDIS
五个字符,标识着一个 RDB 文件的开始。
在读入文件的时候,程序可以通过检查一个文件的前五个字节,来快速地判断该文件是否有可能是 RDB 文件。
RDB-VERSION
一个四字节长的以字符表示的整数,记录了该文件所使用的 RDB 版本号。
因为不同版本的 RDB 文件互不兼容,所以在读入程序时,需要根据版本来选择不同的读入方式。
DB-DATA
这个部分在一个 RDB 文件中会出现任意多次,每个 DB-DATA
部分保存着服务器上一个非空数据库的所有数据。
SELECT-DB
这域保存着跟在后面的键值对所属的数据库号码。
在读入 RDB 文件时,程序会根据这个域的值来切换数据库,确保数据被还原到正确的数据库上。
KEY-VALUE-PAIRS
因为空的数据库不会被保存到 RDB 文件,所以这个部分至少会包含一个键值对的数据。
每个键值对的数据使用以下结构来保存:
+----------------------+---------------+-----+-------+
| OPTIONAL-EXPIRE-TIME | TYPE-OF-VALUE | KEY | VALUE |
+----------------------+---------------+-----+-------+
OPTIONAL-EXPIRE-TIME
域是可选的,如果键没有设置过期时间,那么这个域就不会出现; 反之,如果这个域出现的话,那么它记录着键的过期时间,在当前版本的 RDB 中,过期时间是一个以毫秒为单位的 UNIX 时间戳。
KEY
域保存着键,格式和 REDIS_ENCODING_RAW
编码的字符串对象一样(见下文)。
TYPE-OF-VALUE
域记录着 VALUE
域的值所使用的编码, 根据这个域的指示, 程序会使用不同的方式来保存和读取 VALUE
的值。
EOF
标志着数据库内容的结尾(不是文件的结尾),值为 rdb.h/EDIS_RDB_OPCODE_EOF
(255
)。
CHECK-SUM
RDB 文件所有内容的校验和, 一个 uint_64t
类型值。
REDIS 在写入 RDB 文件时将校验和保存在 RDB 文件的末尾, 当读取时, 根据它的值对内容进行校验。
如果这个域的值为 0
, 那么表示 Redis 关闭了校验和功能。
4. 参考资料
https://mp.weixin.qq.com/s/YjFc307WDD81tJCiw_4Rnw
https://redisbook.readthedocs.io/en/latest/internal/rdb.html
https://mp.weixin.qq.com/s/aCs7JUaS2zihpHt60wbbxA
https://mp.weixin.qq.com/s/lPwPb-d7PZc_2oFvJytsyw