redis RDB持久化

最后更新:2020-04-06

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 功能最核心的是 rdbSaverdbLoad 两个函数, 前者用于生成 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 已经在执行当中。

BGREWRITEAOFBGSAVE 不能同时执行:

  • 如果 BGSAVE 正在执行,那么 BGREWRITEAOF 的重写请求会被延迟到 BGSAVE 执行完毕之后进行,执行 BGREWRITEAOF 命令的客户端会收到请求被延迟的回复。
  • 如果 BGREWRITEAOF 正在执行,那么调用 BGSAVE 的客户端将收到出错信息,表示这两个命令不能同时执行。

BGREWRITEAOF BGSAVE 两个命令在操作方面并没有什么冲突的地方, 不能同时执行它们只是一个性能方面的考虑: 并发出两个子进程, 并且两个子进程都同时进行大量的磁盘写入操作, 这怎么想都不会是一个好主意。

1.2. 自动触发

  1. 通过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 ""即为关闭

  2. 如果从节点执行全量复制操作,主节点自动执行bgsave生产RDB文件并发送给从节点
  3. 执行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

  4. 默认情况下执行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命令的执行流程:

  1. Redis父进程首先判断:当前是否在执行save,或bgsave/bgrewriteaof(后面会详细介绍该命令)的子进程,如果在执行则bgsave命令直接返回。bgsave/bgrewriteaof 的子进程不能同时执行,主要是基于性能方面的考虑:两个并发的子进程同时执行大量的磁盘写操作,可能引起严重的性能问题。
  2. 父进程执行fork操作创建子进程,这个过程中父进程是阻塞的,Redis不能执行来自客户端的任何命令;
  3. 父进程fork后,bgsave命令返回”Background saving started”信息并不再阻塞父进程,并可以响应其他命令;
  4. 子进程创建RDB文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换;
  5. 子进程发送信号给父进程表示完成,父进程更新统计信息。

通过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_EOF255)。

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

Edgar

Edgar
一个略懂Java的小菜比