前言
Redis 是内存数据库,数据都是存储在内存中,为了避免进程退出导致数据的永久丢失,需要定期将 Redis 中的数据以数据或命令的形式从内存保存到本地磁盘。当下次 Redis 重启时,利用持久化文件进行数据恢复。Redis 提供了 RDB 和 AOF 两种持久化机制,前者将当前的数据保存到磁盘,后者则是将每次执行的写命令保存到磁盘(类似于 MySQL 的 Binlog)。本文将详细介绍 RDB 和 AOF 两种持久化方案,包括操作方法和持久化的实现原理。
正文
Redis 是一个基于键值对(K-V)存储的数据库服务器,下面先介绍 Redis 数据库的内部构造以及 K-V 的存储形式,有助于我们更容易理解 Redis 的持久化机制。
1. 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 类型表示。
2. RDB 持久化
RDB 持久化(也称作快照持久化)是指将内存中的数据生成快照保存到磁盘里面,保存的文件后缀是 .rdb。rdb 文件是一个经过压缩的二进制文件,当 Redis 重新启动时,可以读取 rdb 快照文件恢复数据。RDB 功能最核心的是 rdbSave 和 rdbLoad 两个函数, 前者用于生成 RDB 文件并保存到磁盘,而后者则用于将 RDB 文件中的数据重新载入到内存中:
RDB 文件是一个单文件的全量数据,很适合数据的容灾备份与恢复,通过 RDB 文件恢复数据库耗时较短,通常 1G 的快照文件载入内存只需 20s 左右。Redis 提供了手动触发保存、自动保存间隔两种 RDB 文件的生成方式,下面先介绍 RDB 的创建和载入过程。
2.1. RDB 的创建和载入
Redis 服务器默认是通过 RDB 方式完成持久化的,对应 redis.conf 文件的配置项如下:
# RDB文件的名称
dbfilename dump.rdb
# 备份RDB和AOF文件存放路径
dir /usr/local/var/db/redis/
复制代码
2.1.1. 手动触发保存
Redis 提供了两个用于生成 RDB 文件的命令,一个是 SAVE,另一个是 BGSAVE。而触发 Redis 进行 RDB 备份的方式有两种,一种是通过 SAVE 命令、BGSAVE 命令手动触发快照生成的方式,另一种是配置保存时间和写入次数,由 Redis 根据条件自动触发保存操作。
1. SAVE 命令
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 客户端的请求处理。
2. BGSAVE 命令
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 命令生成快照的流程:
- 客户端发起 BGSAVE 命令,Redis 主进程判断当前是否存在正在执行备份的子进程,如果存在则直接返回
- 父进程 fork 一个子进程 (fork 的过程中会造成阻塞的情况),这个过程可以使用 info stats 命令查看 latest_fork_usec 选项,查看最近一次 fork 操作消耗的时间,单位是微秒
- 父进程 fork 完成之后,则会返回 Background saving started 的信息提示,此时 fork 阻塞解除
- fork 创建的子进程开始根据父进程的内存数据生成临时的快照文件,然后替换原文件
- 子进程备份完毕后向父进程发送完成信息,父进程更新统计信息
3. SAVE 和 BGSAVE 的比较
命令 | SAVE | BGSAVE |
---|---|---|
IO 类型 | 同步 | 异步 |
是否阻塞 | 全程阻塞 | fork 时发生阻塞 |
复杂度 | O(n) | O(n) |
优点 | 不会消耗额外的内存 | 不阻塞客户端 |
缺点 | 阻塞客户端 | fork 子进程消耗内存 |
2.1.2. 自动触发保存
因为 BGSAVE 命令可以在不阻塞服务器进程的情况下执行,所以 Redis 的配置文件 redis.conf 提供了一个 save 选项,让服务器每隔一段时间自动执行一次 BGSAVE 命令。用户可以通过 save 选项设置多个保存条件,只要其中任意一个条件被满足,服务器就会执行 BGSAVE 命令。 Redis 配置文件 redis.conf 默认配置了以下 3 个保存条件:
save 900 1
save 3