持久化 RDB 与 AOF
Redis 的运行数据保存在内存中,进程、主机或存储故障会造成数据丢失风险。持久化把数据写到磁盘:RDB 用「快照」整体序列化,AOF 用「追加命令日志」记录状态变化,混合持久化则结合二者。本章讲解三种方案的原理、取舍与生产选型。
1. RDB
1.1 工作原理
RDB 把某个时刻的内存数据整体序列化成一个二进制文件(默认 dump.rdb),恢复时直接把这个文件加载回内存。
恢复时,Redis 启动检测到 dump.rdb 后由主线程加载:加载期间不接收任何命令,文件越大恢复耗时越长。RDB 恢复比 AOF 重放快(直接读二进制、无需逐条执行命令),但并非零延迟——一个数 GB 的快照恢复同样需要数秒到数十秒。这也是 RDB 定位为「冷备 / 灾备 / 迁移」而非「实时保底」的原因。
1.1.1 触发方式
| 方式 | 命令/配置 | 说明 |
|---|---|---|
| 手动保存 | SAVE | 主线程执行并阻塞后续命令,只用于明确需要的维护操作 |
| 手动后台保存 | BGSAVE | fork 子进程执行,不阻塞主线程 |
| 自动保存 | save 3600 1 等 | 满足条件自动触发 BGSAVE |
| 关闭时 | SHUTDOWN SAVE | 正常关闭时自动触发 RDB |
自动触发配置:
save 3600 1 # 3600 秒内至少 1 次写操作
save 300 100 # 300 秒内至少 100 次写操作
save 60 10000 # 60 秒内至少 10000 次写操作满足任意一个条件就触发一次 BGSAVE。如果要禁用 RDB:
save "" # 禁用 RDB 自动触发1.2 fork 与写时复制
BGSAVE 的核心机制是 fork 与写时复制(Copy-On-Write,COW)。
1.2.1 fork 子进程
fork() 创建子进程,子进程复制父进程的内存页表,得到一个「内存快照」。此时父子进程共享同一份内存,并不实际复制数据。
为什么必须 fork 出子进程来做,而不是主线程直接遍历内存写文件?Redis 主进程是单线程处理命令的:如果主线程直接序列化内存,那么在序列化期间(大内存实例可能持续数秒)所有客户端命令都会被阻塞,服务表现为整体卡死。fork 之后由子进程承担序列化与磁盘 IO,主进程继续响应命令,停顿只发生在 fork 调用本身那一瞬间。
fork() 系统调用:
1. 内核复制父进程的页表(不是内存数据)
2. 子进程获得与父进程完全相同的虚拟地址空间
3. 父子进程共享物理内存页(引用计数 +1)
4. 子进程开始遍历数据,写 RDB 文件1.2.2 写时复制(COW)
子进程写 RDB 文件期间,如果主进程继续处理写命令修改了数据,操作系统会为被修改的内存页创建副本:
fork 时刻:
物理页 P1 → 被父子进程共享(引用计数=2)
主进程修改数据:
1. 触发页错误(page fault)
2. 内核为 P1 创建副本 P2
3. 主进程写入 P2(新数据)
4. 子进程继续读 P1(旧数据,快照一致性)COW 同时带来两个收益:内存只复制被修改的页,而非整份数据;子进程读到的始终是 fork 时刻的旧数据,快照内容保持一致。代价是主进程首次写入仍被共享的内存页时触发一次页复制,写入越频繁、触发 COW 的页越多,额外内存越多。
1.2.3 fork 的代价
| 项目 | 说明 |
|---|---|
| fork 耗时 | 取决于内存页表大小,具体值用 latest_fork_usec 实测,见 §1.2.4 |
| 内存开销 | 写入越多,COW 复制越多,极端情况内存接近翻倍 |
| 主线程阻塞 | fork 期间主线程短暂停顿,子进程写文件时不阻塞 |
| CPU 开销 | 子进程遍历数据生成 RDB 消耗 CPU |
生产环境最大坑:实例内存越大,页表越大,
fork耗时越长,fork期间主线程短暂停顿,可能导致客户端请求超时。解决方案是控制单实例内存规模,或用集群分片把数据分散到多个小实例。
另一个易忽略的坑:透明大页(THP)。Linux 默认可能开启透明大页,
fork后触发 COW 时,内核以 2MB 大页为单位复制,而非 4KB 普通页。主进程哪怕只修改几个字节,也可能触发整页复制,导致 RDB 生成期间内存开销远大于实际写入量。Redis 官方文档建议在部署机器上关闭 THP(echo never > /sys/kernel/mm/transparent_hugepage/enabled)。
1.2.4 查看 fork 耗时
redis-cli INFO stats | grep latest_fork_usec
# latest_fork_usec:15234 (fork 耗时 15ms)下表阈值为社区经验参考,非官方硬性标准,应结合自身 SLA 与实测基线调整:
| fork 耗时 | 评估 |
|---|---|
| < 10ms | 健康 |
| 10~100ms | 可接受 |
| > 100ms | 需要关注,可能导致请求超时 |
| > 1s | 严重问题,需拆分数据或优化 |
1.3 RDB 文件结构
RDB 文件是一个自描述的二进制文件:
┌──────────────────────────────────────────┐
│ REDIS + 版本号 │
├──────────────────────────────────────────┤
│ 辅助字段(redis-ver, ctime, used-mem 等) │
├──────────────────────────────────────────┤
│ 数据库 0 │
│ ├── SELECTDB + db 编号 │
│ ├── EXPIRETIME / EXPIRETIME_MS │
│ ├── 类型 + key + value │
│ └── ... │
├──────────────────────────────────────────┤
│ 数据库 1 ... │
├──────────────────────────────────────────┤
│ EOF │
├──────────────────────────────────────────┤
│ 校验和(CRC64) │
└──────────────────────────────────────────┘尾部校验和字段始终存在:开启 rdbchecksum 时写入真实 CRC64,关闭时写入 0,加载时跳过校验,并非省略该字段。
1.4 配置详解
# 文件配置
dbfilename dump.rdb # RDB 文件名
dir /var/lib/redis # 文件保存目录
rdbcompression yes # LZF 压缩(减少文件体积,消耗少量 CPU)
rdbchecksum yes # CRC64 校验(检测文件损坏)
rdb-save-incremental-fsync yes # 增量 fsync(减少写入峰值)
# 触发配置
save 3600 1
save 300 100
save 60 10000| 配置 | 建议 |
|---|---|
rdbcompression | 保持开启,LZF 压缩率高、CPU 开销低 |
rdbchecksum | 保持开启,检测文件损坏 |
rdb-save-incremental-fsync | 大 RDB 文件时建议开启,避免一次性 fsync 卡顿 |
save | 根据数据重要程度调整频率 |
1.5 优缺点
| 维度 | 说明 |
|---|---|
| 优点 | 文件紧凑,二进制格式,加载恢复快(适合灾备) |
| 优点 | fork 子进程生成,主进程不阻塞(BGSAVE) |
| 优点 | 适合全量备份、跨机房迁移 |
| 缺点 | 快照之间宕机会丢失最后一段数据(不实时) |
| 缺点 | fork 大内存实例时有停顿风险 |
| 缺点 | 数据量大时 RDB 文件生成慢 |
2. AOF
2.1 原理
AOF(Append Only File)记录服务器收到的每一条写命令,重启时按顺序重新执行这些命令来重建数据。
2.1.1 写入流程
客户端写命令 → 命令执行 → 追加到 AOF 缓冲区(aof_buf)→ 根据策略刷盘关键点:写命令不是直接写磁盘,而是先写入内存中的 AOF 缓冲区,再根据 appendfsync 策略决定何时刷盘。
2.1.2 三种刷盘策略
| 策略 | 配置值 | 刷盘时机 | 数据安全 | 性能 |
|---|---|---|---|---|
| 每次刷盘 | always | 每次写入后 fsync 到磁盘 | 降低已确认写入因进程或主机故障丢失的风险,仍取决于存储实现 | 最差 |
| 每秒刷盘 | everysec | 每秒 fsync 一次 | 正常故障下通常为秒级窗口 | 较好 |
| 不主动刷盘 | no | 交给操作系统(write 后不 fsync) | 最差,丢多少不可控 | 最好 |
appendfsync everysec # 生产推荐要理解三者的差异,先分清 write 与 fsync 是两层操作:
write():把命令从用户态缓冲区复制到内核页缓存(page cache),此时数据仍在内存中、尚未落盘,断电即丢。fsync():把内核页缓存真正刷写到磁盘,完成后数据才算持久化。
三种策略的底层差异:
always: write() → fsync() → 返回(每条命令都同步到磁盘)
everysec: write() → 返回(先写内核缓冲区)→ 后台线程每秒 fsync()
no: write() → 返回(完全交给操作系统调度 fsync)always 并非每条命令独立执行一次物理磁盘写入,Redis 会把一批待刷盘命令合并后统一 fsync(group commit),再回复客户端。
everysec 在 write 后返回,由后台线程每秒执行一次 fsync。两次 fsync 之间积累的命令在断电或主机故障时可能丢失,通常按约一秒的窗口规划,但这不是严格上限。实际丢失量还取决于文件系统、磁盘、故障类型和恢复流程,不能把配置值当作绝对保证。
everysec通常作为性能与持久化窗口的起点配置,但必须用目标磁盘和真实负载验证。always会显著增加写入延迟,只应在较低 RPO 要求经过业务论证和压测后使用。
2.2 重写
AOF 文件会随着写命令累积越来越大,需要定期「重写」压缩。
2.2.1 为什么要重写
很多命令是冗余的,重写可以把「过程」压缩成「结果」:
重写前:SET a 1; SET a 2; SET a 3; INCR a
重写后:SET a 4 ← 只保留最终结果重写不是分析旧 AOF 文件,而是读取当前内存数据,生成一个只包含最终状态的最小命令集。
2.2.2 重写原理
无论哪个版本,重写的本质相同:fork 子进程读取当前内存数据,生成只包含最终状态的最小表示;重写期间主进程继续处理写命令,并把增量记入重写缓冲区,最后合并进新文件。区别在「新文件」的形态。
Redis 7.0 之前(单文件 AOF):
1. 主进程 fork 子进程
2. 子进程遍历内存数据,生成新 AOF 文件
3. 子进程重写期间,主进程的新写命令追加到:
- 旧 AOF 文件(保证旧文件完整)
- AOF 重写缓冲区(aof_rewrite_buf)
4. 子进程重写完成,主进程把重写缓冲区追加到新文件
5. rename 原子替换旧文件Redis 7.0 起(多文件 AOF,Multi-Part AOF):
AOF 不再是一个文件,而是 appenddirname 目录下的一组文件,由 manifest 清单描述:
- 基础文件(base):重写生成的完整快照,开启
aof-use-rdb-preamble时为 RDB 格式,否则为 AOF 格式。 - 增量文件(incremental):记录基础文件之后的新写命令。
- manifest:描述当前生效的基础文件与增量文件集合。
重写时子进程生成新的基础文件,主进程仍把重写期间的新命令记入重写缓冲区,完成后追加到新基础文件之后,形成新的增量文件;同时更新 manifest 指向新文件集合,再清理旧的 base 与 incremental 文件。
关键点:
| 环节 | 说明 |
|---|---|
| 子进程重写 | fork 子进程后台生成新文件,主进程不阻塞 |
| 增量缓冲 | 重写期间的新写命令记入重写缓冲区,最后合并进新文件;7.0 起为追加到新基础文件之后 |
| 文件切换 | 7.0 前 rename 替换单文件;7.0 起更新 manifest 切换文件集合 |
| fork 代价 | 与 RDB 的 fork 相同,大内存实例有停顿风险 |
2.2.3 触发重写
auto-aof-rewrite-percentage 100 # AOF 增长超过 100% 触发
auto-aof-rewrite-min-size 64mb # 最小触发体积(AOF < 64mb 不触发重写)手动触发:
BGREWRITEAOF2.3 文件损坏修复
AOF 文件可能因为宕机、磁盘故障等原因损坏。Redis 提供了修复工具:
redis-check-aof --fix appendonly.aof # Redis 7.0 之前(单文件)
redis-check-aof --fix appendonly.aof.manifest # Redis 7.0 起(多文件,指向 manifest)修复原理:扫描 AOF 文件,找到第一个格式错误的位置,截断其后的所有内容。Redis 7.0 起 AOF 是目录结构,修复工具作用于 manifest 清单,而非单个 AOF 文件。
修复会丢失损坏点之后的数据。修复前建议先备份原文件。
3. 混合持久化
Redis 4.0 引入混合持久化:AOF 重写时,文件开头是 RDB 快照,之后追加增量 AOF 命令。
混合持久化文件结构
┌─────────────────┬──────────────────────┐
│ RDB 快照头部 │ 后续增量 AOF 命令 │
│ (快速恢复) │ (实时性) │
└─────────────────┴──────────────────────┘Redis 7.0 起的多文件 AOF 中,混合持久化体现为「RDB 格式的基础文件 + AOF 格式的增量文件」,而非单文件内的头尾拼接。
恢复时先加载 RDB 部分(快),再重放 AOF 部分(保证最新数据)。
| 特性 | RDB | AOF | 混合 |
|---|---|---|---|
| 恢复速度 | 快 | 慢(重放命令) | 快(先加载 RDB) |
| 数据安全 | 丢快照间隔 | 取决于 appendfsync 与故障模型 | 取决于 appendfsync 与故障模型 |
| 文件体积 | 小 | 大 | 中等 |
aof-use-rdb-preamble yes # 开启混合持久化(4.0 引入,5.0 起默认开启)4. RDB vs AOF
| 维度 | RDB | AOF |
|---|---|---|
| 数据安全 | 丢失快照间隔的数据 | 取决于 appendfsync 与故障模型 |
| 恢复速度 | 快(直接加载二进制) | 慢(重放命令) |
| 文件体积 | 小(二进制 + 压缩) | 大(文本命令) |
| fork 开销 | 每次快照都要 fork | AOF 重写也要 fork |
| 适用场景 | 灾备恢复、数据迁移 | 数据完整性优先 |
常见起点是同时启用 RDB 和 AOF:RDB 便于备份和快速恢复,AOF 提供更细的恢复点。Redis 同时存在两类文件时优先加载 AOF;是否同时启用仍需结合 RPO、磁盘容量、fork 成本和恢复演练判断。
5. 持久化选型
| 场景 | 推荐方案 |
|---|---|
| 缓存(可丢数据) | 不开启持久化 |
| 数据重要、可容忍少量丢失 | 混合持久化 + everysec |
| RPO 要求接近 0 | AOF always + 副本与备份,且已完成压测和恢复演练 |
| 灾备恢复 | RDB 定期备份 + AOF 提供更细的恢复点 |
5.1 常见坑
| 坑 | 说明 |
|---|---|
| 主节点不开持久化 | 主从切换时新主无数据,数据丢失 |
| 只靠从节点持久化 | 主节点宕机数据全丢,从节点可能也没同步到 |
| 恢复优先级混乱 | 同时存在 RDB 和 AOF 时,Redis 优先用 AOF 恢复 |
| fsync 设置过严 | always 显著增加写入延迟,仅在较低 RPO 经过验证后使用 |
主节点关闭持久化、依赖从节点持久化时,故障转移后可能恢复到更旧的数据。是否在主节点开启 RDB/AOF 应由 RPO 决定;如果不启用,必须明确接受提升副本时的恢复点,并通过副本延迟、备份和恢复演练验证该取舍。
5.2 与备份恢复的分工
本章负责持久化机制、数据丢失风险与配置选型;备份副本、恢复步骤、恢复演练与升级兼容性由备份、恢复与升级章节负责。开启持久化不等同于具备灾备能力,生产部署时应把持久化选型与业务 RPO/RTO、复制拓扑和恢复演练结合评估。