Redis 持久化机制:RDB 与 AOF
Redis 支持 RDB 和 AOF 两种持久化机制。持久化的作用,是避免 Redis 进程退出后造成数据丢失;当 Redis 下次重启时,可以利用之前生成的持久化文件进行数据恢复。
第四章主要围绕两部分展开:一是 RDB、AOF 的配置和运行流程,二是控制持久化过程的相关命令,例如 bgsave 和 bgrewriteaof。下面按照课件顺序,对 Redis 持久化机制进行整理。
4.1 RDB
RDB 持久化是把当前 Redis 进程中的数据生成快照,并保存到硬盘的过程。触发 RDB 持久化的方式分为两类:手动触发和自动触发。
4.1.1 触发机制
手动触发主要对应两个命令:save 和 bgsave。
save 命令会阻塞当前 Redis 服务器,直到 RDB 持久化过程完成为止。如果 Redis 实例占用的内存比较大,这个过程可能造成长时间阻塞,所以实际中基本不采用这种方式。
bgsave 命令会让 Redis 进程执行 fork 操作创建子进程,真正的 RDB 持久化过程由子进程负责,完成后子进程自动结束。它只会在 fork 阶段发生阻塞,一般时间很短。Redis 内部所有涉及 RDB 的操作,基本都采用类似 bgsave 的方式。
除了手动触发,Redis 还支持自动触发 RDB 持久化。在实际使用中,自动触发机制更有价值,主要包括以下几种情况:
- 使用
save配置。例如save m n表示在m秒内,数据集发生了n次修改,就自动触发 RDB 持久化。 - 从节点进行全量复制时,主节点会自动执行 RDB 持久化,然后把 RDB 文件内容发送给从节点。
- 执行
shutdown命令关闭 Redis 时,会执行 RDB 持久化。
4.1.2 流程说明
bgsave 是主流的 RDB 持久化方式,它的运行流程可以概括为以下几步。
首先,执行 bgsave 命令后,Redis 父进程会判断当前是否存在其他正在执行的子进程,例如 RDB 或 AOF 子进程。如果存在,bgsave 命令会直接返回。
然后,父进程执行 fork 创建子进程。fork 过程中父进程会阻塞。可以通过 info stats 命令查看 latest_fork_usec 选项,获取最近一次 fork 操作的耗时,单位是微秒。
当 fork 完成后,bgsave 命令会返回 Background saving started 信息。此时父进程不再被阻塞,可以继续响应其他命令。
接着,子进程开始创建 RDB 文件。它会根据父进程的内存生成临时快照文件,完成后对原有文件进行原子替换。执行 lastsave 命令可以获取最后一次生成 RDB 的时间,对应 info 统计中的 rdb_last_save_time 选项。
最后,子进程会发送信号给父进程表示持久化完成,父进程再更新相关统计信息。
4.1.3 RDB 文件的处理
RDB 文件的处理主要包括保存、压缩和校验三个方面。
在保存方面,RDB 文件会保存在 dir 配置指定的目录下,默认目录是 /var/lib/redis/。文件名由 dbfilename 配置指定,默认是 dump.rdb。Redis 运行期间,也可以通过 config set dir {newDir} 和 config set dbfilename {newFilename} 动态修改保存目录和文件名;下次运行时,RDB 文件就会保存到新的目录中。
在压缩方面,Redis 默认采用 LZF 算法对生成的 RDB 文件进行压缩。压缩后的文件通常远小于内存大小。该功能默认开启,也可以通过 config set rdbcompression {yes|no} 动态修改。虽然压缩 RDB 会消耗 CPU,但它可以大幅降低文件体积,方便保存到硬盘,或者通过网络发送给从节点,因此课件中建议开启。
在校验方面,如果 Redis 启动时加载到损坏的 RDB 文件,会拒绝启动。这时可以使用 Redis 提供的 redis-check-dump 工具检测 RDB 文件,并获取对应的错误报告。
4.1.4 RDB 的优缺点
RDB 是一个紧凑压缩的二进制文件,代表 Redis 在某个时间点上的数据快照。它非常适合用于备份、全量复制等场景。例如,可以每 6 小时执行一次 bgsave 备份,并把 RDB 文件复制到远程机器或者文件系统中,例如 HDFS,用于灾备。
Redis 加载 RDB 恢复数据的速度远远快于 AOF。
不过,RDB 方式没办法做到实时持久化或秒级持久化。因为 bgsave 每次运行都需要执行 fork 创建子进程,这属于重量级操作,频繁执行成本过高。
此外,RDB 文件使用特定的二进制格式保存。Redis 版本不断演进的过程中,出现过多个 RDB 版本,因此兼容性上可能存在风险。
4.2 AOF
AOF 的全称是 Append Only File。AOF 持久化会以独立日志的方式记录每次写命令,Redis 重启时再重新执行 AOF 文件中的命令,从而达到恢复数据的目的。
AOF 的主要作用,是解决数据持久化实时性的问题。目前它已经是 Redis 持久化的主流方式。理解 AOF 持久化机制,有助于在数据安全性和性能之间做出平衡。
4.2.1 使用 AOF
开启 AOF 功能需要设置配置项:
conf
appendonly yes
AOF 默认不开启。AOF 文件名通过 appendfilename 配置设置,默认是 appendonly.aof。它的保存目录和 RDB 持久化方式一致,都是通过 dir 配置指定。
AOF 的工作流程包括四个步骤:命令写入、文件同步、文件重写和重启加载。
- 所有写入命令会追加到
aof_buf缓冲区中。 - AOF 缓冲区会根据对应策略向硬盘做同步操作。
- 随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
- 当 Redis 服务器启动时,可以加载 AOF 文件进行数据恢复。
4.2.2 命令写入
AOF 命令写入的内容是文本协议格式。以 set hello world 为例,在 AOF 缓冲区中会追加如下文本:
text
*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n
这里遵守 Redis 协议格式。Redis 选择文本协议,课件中给出的原因主要有三个:文本协议具备较好的兼容性,实现简单,并且具备可读性。
AOF 过程中之所以需要 aof_buf 缓冲区,是因为 Redis 使用单线程响应命令。如果每次写 AOF 文件都直接同步硬盘,性能就会从内存读写变成 IO 读写,必然下降。先写入缓冲区可以有效减少 IO 次数,同时 Redis 还可以提供多种缓冲区同步策略,让使用者根据需要做出平衡。
4.2.3 文件同步
Redis 提供了多种 AOF 缓冲区同步文件策略,由 appendfsync 参数控制。
| 可配置值 | 说明 |
|---|---|
always |
命令写入 aof_buf 后调用 fsync 同步,完成后返回 |
everysec |
命令写入 aof_buf 后只执行 write 操作,不进行 fsync,每秒由同步线程进行 fsync |
no |
命令写入 aof_buf 后只执行 write 操作,由 OS 控制 fsync 频率 |
这里涉及两个系统调用:write 和 fsync。
write 操作会触发延迟写机制。Linux 在内核中提供页缓冲区,用来提升硬盘 IO 性能。write 操作写入系统缓冲区后会立即返回,真正同步到硬盘依赖系统调度机制,例如缓冲区页空间写满,或者达到特定时间周期。如果同步文件之前系统发生故障宕机,缓冲区内的数据就会丢失。
fsync 针对单个文件操作,会强制进行硬盘同步。fsync 会一直阻塞,直到数据写入硬盘。
如果配置为 always,每次写入都要同步 AOF 文件,性能很差。在一般的 SATA 硬盘上,只能支持大约几百 TPS 写入。除非数据非常重要,否则不建议配置。
如果配置为 no,由于操作系统同步策略不可控,虽然性能提高了,但数据丢失风险也大幅增加。除非数据重要程度很低,一般也不建议配置。
如果配置为 everysec,这是默认配置,也是推荐配置。它兼顾了数据安全性和性能,理论上最多丢失 1 秒的数据。
4.2.4 重写机制
随着命令不断写入 AOF,文件会越来越大。为了解决这个问题,Redis 引入了 AOF 重写机制,用来压缩文件体积。
AOF 文件重写,是把 Redis 进程内的数据转化为写命令,并同步到新的 AOF 文件中。
重写后的 AOF 文件之所以可以变小,主要有三个原因:
- 进程内已经超时的数据不再写入文件。
- 旧 AOF 中的无效命令,例如
del、hdel、srem等,重写后会被删除,只需要保留数据的最终版本。 - 多条写操作可以合并为一条,例如
lpush list a、lpush list b、lpush list c可以合并为lpush list a b c。
较小的 AOF 文件一方面可以降低硬盘空间占用,另一方面也可以提升 Redis 启动时数据恢复的速度。
AOF 重写过程可以手动触发,也可以自动触发。
手动触发是调用 bgrewriteaof 命令。
自动触发由 auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 参数决定。其中,auto-aof-rewrite-min-size 表示触发重写时 AOF 的最小文件大小,默认是 64MB;auto-aof-rewrite-percentage 代表当前 AOF 占用大小相比上次重写时增加的比例。
当触发 AOF 重写时,运行流程如下。
首先,执行 AOF 重写请求。如果当前进程正在执行 AOF 重写,请求不会执行;如果当前进程正在执行 bgsave 操作,重写命令会延迟到 bgsave 完成之后再执行。
然后,父进程执行 fork 创建子进程。
fork 之后,主进程继续响应其他命令。所有修改操作会写入 AOF 缓冲区,并根据 appendfsync 策略同步到硬盘,以保证旧 AOF 文件机制正确。由于子进程只拥有 fork 之前的内存信息,所以父进程还需要把 fork 之后这段时间的修改操作写入 AOF 重写缓冲区。
接着,子进程根据内存快照,把命令合并到新的 AOF 文件中。
最后,子进程完成重写后,会发送信号给父进程。父进程把 AOF 重写缓冲区内临时保存的命令追加到新的 AOF 文件中,然后用新的 AOF 文件替换旧的 AOF 文件。
4.2.5 启动时数据恢复
当 Redis 启动时,会根据 RDB 和 AOF 文件的内容进行数据恢复。
课件中的恢复流程可以整理为:启动 Redis 后,先判断是否开启 AOF。如果开启 AOF,则判断是否存在 AOF 文件,存在时加载 AOF;如果没有开启 AOF,则判断是否存在 RDB 文件,存在时加载 RDB。加载成功则启动成功,加载失败则启动失败。
4.3 本章重点回顾
Redis 提供了两种持久化方案:RDB 和 AOF。
RDB 可以视为内存快照。它生成的内容更加紧凑,占用空间较小,恢复速度更快。但是生成 RDB 的开销较大,不适合进行实时持久化,一般用于冷备和主从复制。
AOF 可以视为对修改命令的保存。Redis 恢复数据时需要重放这些命令,并且 AOF 有重写机制,可以定期压缩 AOF 文件。
RDB 和 AOF 都会使用 fork 创建子进程,利用 Linux 子进程拥有父进程内存快照的特点进行持久化,从而尽可能不影响主进程继续处理后续命令。