RDB/AOF------Redis两大持久化方法
Redis ⽀持RDB和AOF两种持久化机制,持久化功能有效地避免因进程退出造成数据丢失问题, 当下次重启时利用之前持久化的文件即可实现数据恢复。
RDB
RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发。
触发机制
手动触发
手动触发分别对应 save 和 bgsave 命令:
- save命令:阻塞当前Redis服务器,直到RDB过程完成为⽌,对于内存⽐较⼤的实例造成⻓时间 阻塞,基本不采用。
- bgsave命令:Redis进程执行fork操作创建⼦进程,RDB持久化过程由⼦进程负责,完成后自动结束。阻塞只发⽣在fork阶段,⼀般时间很短
Redis 内部的所以涉及RDB的操作都采用类似bgsave的方式
自动触发
即Redis运行自动触发了RDB持久化机制
- 使用save配置:
save m n
表示m秒内数据集发生了n次修改,自动RDB持久化 - 主从节点进行全量复制的时候,主节点自动进行RDB持久化,随后将RDB文件发送给从节点
- 使用
shutDown
命令关闭Redis,自动进行RDB持久化
执行流程
- 在执行
bgsave
命令的时候,Redis父进程判断当前是否存在其他正在执行例如RDB/AOF的子进程,如果存在的话bgsave
命令会直接返回 - 父进程直接
fock
创建子进程的时候,fork
过程中父进程会阻塞,通过infostats命令查看 latest_fork_usec 选项,可以获取最近⼀次fork操作的耗时,单位为微秒。 - 父进程fork完成后,bgsave命令返回
Background saving started
信息不会再阻塞父进程,可以继续响应其他命令。 - 子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原⼦替换。执 ⾏
lastsave
命令可以获取最后⼀次生成RDB的时间,对应info统计的rdb_last_save_time
选项 - 子进程发送信号给父进程表示完成,父进程更新统计的信息。
RDB 文件的处理
保存: RDB 文件保存在 dir
配置指定的目录(默认 /var/lib/redis/
)下,文件名通过 dbfilename
配置(默认 dump.rdb
)指定。可以通过执行 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 文件并获取对应的错误报告。
RDB 的优缺点
-
RDB 是一个紧凑压缩的二进制文件,代表 Redis 在某个时间点上的数据快照。非常适用于备份,全量复制等场景。比如每 6 小时执行
bgsave
备份,并把 RDB 文件复制到远程机器或者文件系统中(如 hdfs)用于灾备。 -
Redis 加载 RDB 恢复数据远远快于 AOF 的方式。
-
RDB 方式没办法做到实时持久化 / 秒级持久化。因为
bgsave
每次运行都要执行fork
创建子进程,属于重量级操作,频繁执行成本过高。 -
RDB 文件使用特定二进制格式保存,Redis 版本演进过程中有多个 RDB 版本,兼容性可能有风险。
AOF
AOF(Append Only File)持久化:以独立日志的方式记录每次写命令,重启时再重新执行 AOF 文件中的命令达到恢复数据的目的。AOF 的主要作用是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式。
AOF配置
开启 AOF 功能需要设置配置:appendonly yes
,默认不开启。AOF 文件名通过 appendfilename
配置(默认是 appendonly.aof
)设置。保存目录同 RDB 持久化方式一致,通过 dir
配置指定。AOF 的工作流程操作:命令写入(append
)、文件同步(sync
)、文件重写(rewrite
)、重启加载(load
)。
执行流程
- 所有的写入命令会追加到
aof_buf
(缓冲区)中。 - AOF 缓冲区根据对应的策略向硬盘做同步操作。
- 随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
- 当 Redis 服务器启动时,可以加载 AOF 文件进行数据恢复。
命令写入
AOF 命令写入的内容直接是文本协议格式。例如 set hello world
这条命令,在 AOF 缓冲区会追加如下文本:
*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n
此处遵守 Redis 格式协议,Redis 选择文本协议可能的原因:文本协议具备较好的兼容性;实现简单;具备可读性。
文件同步
[!QUESTION] 思考一下AOF 过程中为什么需要 AOF 这个缓冲区?
Redis 使用单线程响应命令,如果每次写 AOF 文件都直接同步硬盘,性能从内存的读写变成 IO 读写,必然会下降。先写入缓冲区可以有效减少 IO 次数,同时,Redis 还可以提供多种缓冲区同步策略,让用户根据自己的需求做出合理的平衡。
Redis 提供了多种 AOF 缓冲区同步策略,由参数 appendfsync
控制,不同值的含义如下表所示。
可配置值 | 说明 |
---|---|
always | 命令写入 aof_buf 后调用 fsync 同步,完成后返回,即每次执行命令之后都将日志数据写回硬盘 |
everysec | 命令写入 aof_buf 后只执行 write 操作,不进行 fsync 。每秒由同步线程进行 fsync 。即每一秒将缓冲区的内容写回到硬盘 |
no | 命令写入 aof_buf 后只执行 write 操作,由 OS 控制缓冲区 fsync 频率。即由操作系统决定什么时候把缓冲区的内容写回硬盘 |
系统调用 write
和 fsync
说明:
write
操作会触发延迟写(delayed write)机制。Linux 在内核提供页缓冲区用来提供硬盘 IO 性能。write
操作在写入系统缓冲区后立即返回。同步硬盘操作依赖于系统调度机制,例如:缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失。fsync
针对单个文件操作,做强制硬盘同步,fsync
将阻塞直到数据写入到硬盘。- 配置为 always 时,每次写入都要同步 AOF 文件,性能很差,在一般的 SATA 硬盘上,只能支持大约几百 TPS 写。除非是非常重要的数据库,否则不建议配置。
- 配置为 no 时,由于操作系统同步策略不可控,虽然提高了性能,但数据丢失风险大增,除非数据重要程度很低,一般不建议配置。
- 配置为 everysec,是默认配置,也是推荐配置,兼顾了数据安全性和性能。理论上最多丢失 1 秒的数据。
重写机制
随着命令不断写入 AOF,文件会越来越大,为了解决这个问题,Redis 引入 AOF 重写机制压缩文件体积。AOF 文件重写是把 Redis 进程内的数据转化为写命令同步到新的 AOF 文件,较小的 AOF 文件一方面降低了硬盘空间占用,另一方面可以提升启动 Redis 时数据恢复的速度。
[!QUESTION] 重写后的 AOF 为什么可以变小?有如下原因:
- 进程内已超时的数据不再写入文件。
- 旧的 AOF 中的无效命令,例如
del
、hdel
、srem
等重写后将会删除,只需要保留保留数据的最终版本。- 多条写操作合并为一条,例如
lpush list a
、lpush list b
、lpush list c
可以合并为lpush list a b c
。
AOF 重写过程也可以分为手动触发和自动触发:
手动触发
调用 bgrewriteaof
命令。
自动触发
根据 auto-aof-rewrite-min-size
和 auto-aof-rewrite-percentage
参数确定自动触发时机。
auto-aof-rewrite-min-size
:表示触发重写时 AOF 的最小文件大小,默认认为 64MB。auto-aof-rewrite-percentage
:代表当前 AOF 占用大小相比上次重写时增加的比例。- 两个参数可以在
redis.conf
中进行配置
重写流程图
- 执行 AOF 重写请求。
如果当前进程正在执行 AOF 重写,请求不执行。如果当前进程正在执行 bgsave 操作,重写命令延迟到 bgsave 完成之后再执行,与RDB的思路一样 - 父进程执行
fork
创建子进程。 - 开始重写
- 主进程
fork
之后,继续响应其他命令。这些命令会被同时写入两个地方:AOF 缓冲区(aof_buf
) 和 AOF 重写缓冲区(aof_rewrite_buf
) 。 并根据appendfsync
策略同步到硬盘,保证旧 AOF 文件机制正确。 - 子进程根据
fork
瞬间的内存快照,开始将数据转化为新的命令并写入临时文件。
- 主进程
- 子进程根据内存快照,将命令合并到新的 AOF 文件中。
- 子进程完成重写
- 新文件写入后,子进程发送信号给父进程。
- 父进程把 AOF 重写缓冲区内临时保存的命令追加到新 AOF 文件中。
- 最后,用这个完整的新文件原子性地替换掉旧的 AOF 文件。
思考
- 那么子进程如何要怎么拥有父进程的数据副本呢?
-
当主进程
fork
一个子进程时,操作系统不会复制所有物理内存,这太慢了。它只会复制主进程的页表。页表记录了虚拟地址和物理地址的映射关系。这意味着,父子进程虽然有独立的虚拟地址空间,但它们都指向同一份物理内存
-
为了安全,这部分共享的物理内存会被标记为只读。这样,父子进程都无法修改对方的数据。
-
当父进程或子进程中的任何一个试图向这些共享内存发起写操作时,就会触发一个写保护中断。
- 什么是写保护中断(Copy On Write)?
当父进程或子进程试图修改共享的物理内存时,操作系统会触发一个写保护中断。这时,操作系统会执行以下操作
- 复制物理内存: 操作系统会为正在发起写操作的进程(无论是父进程还是子进程)复制一份它想要写入的物理内存页面。
- 更新页表: 操作系统会修改该进程的页表,将原来指向共享物理内存的条目,重新指向新复制出来的、独立的物理内存页面。
- 设置权限: 新复制的物理页面会被设置为可读可写权限。
- 为什么需要
aof_rewrite_buf
和aof_buf
?
这是为了保证数据在整个重写过程中的完整性。
aof_buf
:这个缓冲区是用来保障旧 AOF 文件的。重写期间,主进程依然会把新命令写入这个缓冲区,并同步到旧的 AOF 文件中。这样,即使在重写完成前发生宕机,我们依然能用完整的旧 AOF 文件来恢复数据,确保万无一失。aof_rewrite_buf
:这个缓冲区是用来保障新 AOF 文件的。子进程开始重写时,它只知道那一刻的数据状态。而重写需要一段时间,这段时间内产生的所有新命令都会被主进程记录在这个临时缓冲区里。当子进程完成任务后,主进程会把aof_rewrite_buf
里的内容追加到新文件里,确保新文件包含了所有最新的、增量的数据。
简单来说,aof_buf
保障的是“现在”,而aof_rewrite_buf
保障的是“未来”。
- 什么时候会导致阻塞父进程
- 在创建子进程的过程中,由于要复制父进程的页表等数据,这里必然会发生阻塞,阻塞时间由页表的大小决定
- 如果触发写时复制,那么期间会进行拷贝内存,内存越大需要时间越长
- 为什么 AOF 重写和 bgsave 必须用
fork
子进程?
Redis 的主进程是单线程的,它需要处理所有的客户端请求。任何耗时的操作(如磁盘 I/O)都会阻塞主进程,导致 Redis 无法响应其他请求,这很显然和Redis高性能的初衷背道而驰。
bgsave
(后台 RDB 快照)和 bgrewriteaof
(后台 AOF 重写)都是非常耗时的 I/O 密集型操作。如果直接在主线程里做,Redis 就“卡死”了。所以必须在后台进行。
- 那么为什么后台任务用子进程 (
fork
) 而不是子线程 (thread)?
- 子进程发生错误不会影响到主进程(稳定性)
- 进程拥有独立的内存空间和资源。如果子进程在进行持久化操作时崩溃了(例如,磁盘满了、权限问题等),操作系统会清理掉这个子进程,而主进程完全不受影响,可以继续为客户端提供服务。
- 如果是多线程模型,所有线程共享同一个进程的内存空间。一个线程的崩溃(如非法内存访问)可能会导致整个进程(包括主线程)的崩溃,这对 Redis 来说是致命的。
- 用上操作系统的写时复制 (Copy-on-Write, COW)
- 这是最关键的一点。当
fork()
创建一个子进程时,它并不会立即复制父进程的所有内存。相反,子进程和父进程会共享相同的物理内存页。 - 只有当父进程或子进程试图修改某个内存页时,操作系统才会真正地复制一份这个内存页,让父子进程各自拥有一份。
- 这对 Redis 持久化意味着什么?
fork
的瞬间是非常快的,因为它不涉及大量的内存复制。- 子进程拥有了创建快照那一刻的完整内存视图,可以从容地将数据写入磁盘。
- 主进程可以继续处理写请求。当写请求修改数据时,“Copy-on-Write” 机制会确保主进程修改的是自己的数据副本,不会影响到子进程正在读取的旧数据。这保证了数据的一致性和隔离性。
- 这个机制以极低的成本实现了“快照”功能,并允许主进程在快照期间继续工作。
- 这是最关键的一点。当
- 避免线程之间的安全问题
- 如果使用多线程,主线程和后台的持久化线程会共享数据。为了防止数据在读写时发生冲突(例如,持久化线程正在读取一个 key,主线程却要删除它),就必须引入复杂的锁机制。
- 加锁会严重影响性能,并且容易产生死锁等并发问题。这与 Redis 追求极致简单和高性能的设计哲学相悖。
- 通过
fork
子进程,父子进程的内存空间在逻辑上是隔离的(通过 COW 机制)。它们之间不需要加锁,主进程可以无锁地处理命令,子进程也可以无锁地读取数据,设计模型非常简洁、干净。
启动时数据恢复
当 Redis 启动时,会根据 RDB 和 AOF 文件的内容,进行数据恢复,如下流程图所示。
两者区别
- RDB 视为内存的快照,产生的内容更为紧凑,占用空间较小,恢复时速度更快。但产生 RDB 的开销较大,不适合进行实时持久化,一般用于冷备和主从复制。
- AOF 视为对修改命令保存,在恢复时需要重放命令。并且有重写机制来定期压缩 AOF 文件。
- RDB 和 AOF 都使用
fork
创建子进程,利用 Linux 子进程拥有父进程内存快照的特点进行持久化,尽可能不影响主进程继续处理后续命令。
混合持久化
在 Redis 4.0 之前,开发者必须在两种持久化方式中做出选择,为了结合两者的优点,有了混合持久化即:[ RDB 格式的全量数据 ] + [ AOF 格式的增量命令 ]
- 重写过程:当触发 AOF 重写时,
fork
出的子进程不再是直接将内存中的数据转换成一条条SET
命令,而是先将当前内存中的数据以 RDB 的格式写入到新的 AOF 文件的开头。 - 增量数据:在子进程写入 RDB 部分的期间,主进程接收到的新的写命令会像之前一样被缓存起来(在 AOF 重写缓冲区)。当子进程完成了 RDB 部分的写入后,主进程会将这部分缓存的增量命令以 AOF 的格式追加到新文件的末尾。
- 文件切换:最后,用这个包含 RDB 和 AOF 内容的新文件替换掉旧的 AOF 文件。
拓展的好处: - 加载速度快:重启加载数据时,Redis 只需加载文件头部的 RDB 部分,这和直接加载一个 RDB 文件速度一样快。
- 数据丢失少:加载完 RDB 部分后,再接着加载文件尾部的 AOF 增量命令。这部分命令记录了从 RDB 快照时间点到 Redis 最后一次写入之间的数据,极大地减少了数据丢失的风险。
- 最终,得到了接近 RDB 的加载性能和接近 AOF 的数据安全性。