Skip to content

持久化

RDB

因为 Redis 是内存数据库,它将自己的数据库状态储存在内存里面,所以如果不想办法将储存在内存中的数据库状态保存到磁盘里面,那么一旦服务器进程退出,服务器中的数据库状态也会消失不见

为了解决这个问题,Redis 提供了 RDB 持久化功能,这个功能可以将 Redis 在内存中数据库状态保存到磁盘里面,避免数据意外丢失

RDB 持久化既可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个 RDB 文件中

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

因为 RDB 文件是保存在硬盘里面的,所以即使 Redis 服务器进程退出,甚至运行 Redis 服务器的计算机停机,但只要 RDB 文件仍然存在,Redis 服务器就可以用它来还原数据库状态。

保存文件的位置

如果 redis.conf 按默认配置:

1
dbfilename dump.rdb

这时 dump.rdb 存放在启动 redis 时的目录

RDB 文件的创建与载入

有两个 Redis 命令可以用于生成 RDB 文件,一个是 SAVE,另一个是BGSAVE

SAVE命令会阻塞 Redis 服务器进程,直到 RDB 文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求:

1
2
redis> SAVE  // 等待直到 RDB 文件创建完毕
OK

SAVE命令直接阻塞服务器进程的做法不同,BGSAVE命令会派生出一个子进程,然后由子进程负责创建 RDB 文件,服务器进程(父进程)继续处理命令请求:

1
2
redis> BGSAVE   // 派生子进程,并由子进程创建 RDB 文件  
Background saving started

创建 RDB 文件的实际工作由 rdb.c/rdbSave函数完成,SAVE命令和BGSAVE命令会以不同的方式调用这个函数,通过以下伪代码可以明显地看出这两个命令之间的区别:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def SAVE():
    # 创建 RDB 文件
    rdbSave()

def BGSAVE():
    # 创建子进程
    pid = fork()
    if pid == 0:
        # 子进程负责创建 RDB 文件
        rdbSave()
        # 完成之后向父进程发送信号
        signal_parent()
    elif pid > 0:
        # 父进程继续处理命令请求,并通过轮询等待子进程的信号  
        handle_request_and_wait_signal()
    else:
        # 处理出错情况
        handle_fork_error()

和使用 SAVE 命令或者BGSAVE命令创建 RDB 文件不同,RDB 文件的载入工作是在服务器启动时自动执行的,所以 Redis 并没有专门用于载入 RDB 文件的命令,只要 Redis 服务器在启动时检测到 RDB 文件存在,它就会自动载入 RDB 文件。

以下是 Redis 服务器启动时打印的日志记录,其中第二条日志 DB loaded from disk: ...就是服务器在成功载入 RDB 文件之后打印的

1
2
3
4
$ redis-server
59568:M 17 Mar 2020 08:35:03.066 # Server initialized
59568:M 17 Mar 2020 08:35:03.073 * DB loaded from disk: 0.007 seconds
59568:M 17 Mar 2020 08:35:03.073 * Ready to accept connections

另外值得一提的是,因为 AOF 文件的更新频率通常比 RDB 文件的更新频率高,所以:

  • 如果服务器开启了 AOF 持久化功能,那么服务器会优先使用 AOF 文件来还原数据库状态
  • 只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态

自动间隔性保存

因为 BGSAVE 命令可以在不阻塞服务器进程的情况下执行,所以 Redis 允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。

用户可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器会执行BGSAVE命令

举个例子,如果我们向服务器提供以下配置:

1
2
3
save 900 1
save 300 10
save 60 10000

那么只要满足以下三个条件中的任意一个,BGSAVE命令就会被执行:

  • 服务器在 900 秒之内,对数据库进行了至少 1 次修改
  • 服务器在 300 秒之内,对数据库进行了至少 10 次修改
  • 服务器在 60 秒之内,对数据库进行了至少 10000 次修改
1
2
3
4
5
16039:M 03 Apr 2020 11:26:11.019 * 1 changes in 900 seconds. Saving...
16039:M 03 Apr 2020 11:26:11.020 * Background saving started by pid 20056
20056:C 03 Apr 2020 11:26:11.056 * DB saved on disk
20056:C 03 Apr 2020 11:26:11.058 * RDB: 0 MB of memory used by copy-on-write
16039:M 03 Apr 2020 11:26:11.121 * Background saving terminated with success
1
2
3
4
59568:M 03 Apr 2020 08:33:55.092 * 10000 changes in 60 seconds. Saving...
59568:M 03 Apr 2020 08:33:55.093 * Background saving started by pid 8016
8016:C 03 Apr 2020 08:33:55.100 * DB saved on disk
59568:M 03 Apr 2020 08:33:55.197 * Background saving terminated with success

优缺点

优点:

RDB 是一个紧凑压缩压缩的二进制文件,代表 Redis 在某个时间点上的数据快照。
非常适用于备份,全量复制等场景。比如每 6 小时执行 bgsave 备份,并把 RDB 文件拷贝到远程机器或者文件系统中(如 hdfs),用于灾难恢复

Redis 加载 RDB 恢复数据远远快于 AOF 的方式

缺点:

RDB 方式数据没办法做到实时持久化/秒级持久化。
因为 bgsave 每次运行都要执行 fork 操作创建子进程,属于重量级操作,频繁执行成本过高

RDB 文件使用特定二进制格式保存,Redis 版本演进过程中有多个格式的 RDB 版本,存在老版本 Redis 服务无法兼容新版 RDB 格式的问题

AOF

AOF(append only file)持久化: 以独立日志的方式记录每次写命令,重启时再重新执行 AOF 文件中的命令达到恢复数据的目的。
AOF 的主要作用是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式。
理解掌握好 AOF 持久化机制对我们兼顾数据安全性和性能非常有帮助

使用 AOF

开启 AOF 功能需要设置配置: appendonly yes,默认不开启。
AOF 文件名通过 appendfilename 配置设置,默认文件名是 appendonly.aof。
保存路径同 RDB 持久化方式一致,通过 dir 配置指定。
AOF 的工作流程操作: 命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load)

流程如下:

(1) 所有的写入命令会追加到 aof_buf(缓冲区)中。
(2) AOF 缓冲区根据对应的策略向硬盘做同步操作
(3) 随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
(4) 当 Redis 服务器重启时,可以加载 AOF 文件进行数据恢复

命令写入

AOF 命令写入的内容直接是文本协议格式。
例如 set hello world 这条命令,在 AOF 缓冲区会追加如下文本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
*2
$6
SELECT
$1
0
*3
$3
set
$5
hello
$5
world

下面介绍关于 AOF 的两个疑惑:

(1) AOF 为什么直接采用文本协议格式?可能的理由如下:

  • 文本协议具有很好的兼容性
  • 开启 AOF 后,所有写入命令都包含追加操作,直接采用协议格式,避免了二次处理开销
  • 文本协议具有可读性,方便直接修改和处理

(2) AOF 为什么把命令追加到 aof_buf 中?

Redis 使用单线程响应命令,如果每次写 AOF 文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负载。
先写入缓冲区 aof_buf 中,还有另一个好处,Redis 可以提供多种缓冲区同步硬盘的策略,在性能和安全性方面做出平衡。

文件同步

问题定位与优化

Redis 持久化一直是影响 Redis 性能的高发地

fork 操作

当 Redis 做 RDB 或 AOF 重写时,一个必不可少的操作就是执行 fork 操作创建子进程,对于大多数操作系统来说 fork 是个重量级错误。
虽然 fork 创建子进程不需要拷贝父进程的物理内存空间,但是会复制父进程的空间内存页表。
例如对于 10GB 的 Redis 进程,需要复制大约 20MB 的内存页表,因此 fork 操作耗时跟进程总内存量息息相关,如果使用虚拟化技术,特别是 Xen 虚拟机,fork 操作会更耗时。

fork 耗时问题定位:

对于高流量的 Redis 实例 OPS 可达 5 万以上,如果 fork 操作耗时在秒级别将拖慢 Redis 几万条命令执行,对线上应用延迟影响非常明显。
正常情况下 fork 耗时应该是每 GB 消耗 20 毫秒左右。
可以在 info stats 统计中查 latest_fork_usec 指标获取最近一次 fork 操作耗时,单位微秒

如何改善 fork 操作的耗时:

(1) 优先使用物理机或者高效支持 fork 操作的虚拟化技术,避免使用 Xen
(2) 控制 Redis 实例最大可用内存,fork 耗时跟内存量成正比,线上建议每个 Redis 实例内存控制在 10GB 以内
(3) 合理配置 Linux 内存分配策略,避免物理内存不足导致 fork 失败
(4) 降低 fork 操作的频率,如适度放宽 AOF 自动触发时机,避免不必要的全量复制等。

子进程开销监控和优化

子进程负责 AOF 或者 RDB 文件的重写,它的运行过程主要涉及 CPU、内存、硬盘三部分的消耗

CPU:

CPU 开销分析。子进程负责把进程内的数据分批写入文件,这个过程属于 CPU 密集操作,通常子进程对单核 CPU 利用率接近 90%

CPU 消耗优化。Redis 是 CPU 密集型服务,不要做绑定单核 CPU 操作。
由于子进程非常消耗 CPU,会和父进程产生单核资源竞争

不要和其他 CPU 密集型服务部署在一起,造成 CPU 过度竞争

如果部署多个 Redis 实例,尽量保证同一时刻只有一个子进程执行重写工作。

内存

硬盘

多实例部署

Redis 单线程架构导致无法充分利用 CPU 多核特性,通常的做法是在一台机器上部署多个 Redis 实例。
当多个实例开启 AOF 重写后,彼此之间会产生对 CPU 和 IO 的竞争