Skip to content

持久化 RDB 与 AOF ​

Redis 的运行数据保存在内存中,进程、主机或存储故障会造成数据丢失风险。持久化把数据写到磁盘:RDB 用「快照」整体序列化,AOF 用「追加命令日志」记录状态变化,混合持久化则结合二者。本章讲解三种方案的原理、取舍与生产选型。

1. RDB ​

1.1 工作原理 ​

RDB 把某个时刻的内存数据整体序列化成一个二进制文件(默认 dump.rdb),恢复时直接把这个文件加载回内存。

恢复时,Redis 启动检测到 dump.rdb 后由主线程加载:加载期间不接收任何命令,文件越大恢复耗时越长。RDB 恢复比 AOF 重放快(直接读二进制、无需逐条执行命令),但并非零延迟——一个数 GB 的快照恢复同样需要数秒到数十秒。这也是 RDB 定位为「冷备 / 灾备 / 迁移」而非「实时保底」的原因。

1.1.1 触发方式 ​

方式命令/配置说明
手动保存SAVE主线程执行并阻塞后续命令,只用于明确需要的维护操作
手动后台保存BGSAVEfork 子进程执行,不阻塞主线程
自动保存save 3600 1 等满足条件自动触发 BGSAVE
关闭时SHUTDOWN SAVE正常关闭时自动触发 RDB

自动触发配置:

bash
save 3600 1      # 3600 秒内至少 1 次写操作
save 300 100     # 300 秒内至少 100 次写操作
save 60 10000   # 60 秒内至少 10000 次写操作

满足任意一个条件就触发一次 BGSAVE。如果要禁用 RDB:

bash
save ""         # 禁用 RDB 自动触发

1.2 fork 与写时复制 ​

BGSAVE 的核心机制是 fork 与写时复制(Copy-On-Write,COW)。

1.2.1 fork 子进程 ​

fork() 创建子进程,子进程复制父进程的内存页表,得到一个「内存快照」。此时父子进程共享同一份内存,并不实际复制数据。

为什么必须 fork 出子进程来做,而不是主线程直接遍历内存写文件?Redis 主进程是单线程处理命令的:如果主线程直接序列化内存,那么在序列化期间(大内存实例可能持续数秒)所有客户端命令都会被阻塞,服务表现为整体卡死。fork 之后由子进程承担序列化与磁盘 IO,主进程继续响应命令,停顿只发生在 fork 调用本身那一瞬间。

txt
fork() 系统调用:
  1. 内核复制父进程的页表(不是内存数据)
  2. 子进程获得与父进程完全相同的虚拟地址空间
  3. 父子进程共享物理内存页(引用计数 +1)
  4. 子进程开始遍历数据,写 RDB 文件

1.2.2 写时复制(COW) ​

子进程写 RDB 文件期间,如果主进程继续处理写命令修改了数据,操作系统会为被修改的内存页创建副本:

txt
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 耗时 ​

bash
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 文件是一个自描述的二进制文件:

txt
┌──────────────────────────────────────────┐
│ REDIS + 版本号                            │
├──────────────────────────────────────────┤
│ 辅助字段(redis-ver, ctime, used-mem 等)  │
├──────────────────────────────────────────┤
│ 数据库 0                                  │
│   ├── SELECTDB + db 编号                  │
│   ├── EXPIRETIME / EXPIRETIME_MS         │
│   ├── 类型 + key + value                  │
│   └── ...                                │
├──────────────────────────────────────────┤
│ 数据库 1 ...                              │
├──────────────────────────────────────────┤
│ EOF                                      │
├──────────────────────────────────────────┤
│ 校验和(CRC64)                            │
└──────────────────────────────────────────┘

尾部校验和字段始终存在:开启 rdbchecksum 时写入真实 CRC64,关闭时写入 0,加载时跳过校验,并非省略该字段。

1.4 配置详解 ​

bash
# 文件配置
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 写入流程 ​

txt
客户端写命令 → 命令执行 → 追加到 AOF 缓冲区(aof_buf)→ 根据策略刷盘

关键点:写命令不是直接写磁盘,而是先写入内存中的 AOF 缓冲区,再根据 appendfsync 策略决定何时刷盘。

2.1.2 三种刷盘策略 ​

策略配置值刷盘时机数据安全性能
每次刷盘always每次写入后 fsync 到磁盘降低已确认写入因进程或主机故障丢失的风险,仍取决于存储实现最差
每秒刷盘everysec每秒 fsync 一次正常故障下通常为秒级窗口较好
不主动刷盘no交给操作系统(write 后不 fsync)最差,丢多少不可控最好
bash
appendfsync everysec   # 生产推荐

要理解三者的差异,先分清 write 与 fsync 是两层操作:

  • write():把命令从用户态缓冲区复制到内核页缓存(page cache),此时数据仍在内存中、尚未落盘,断电即丢。
  • fsync():把内核页缓存真正刷写到磁盘,完成后数据才算持久化。

三种策略的底层差异:

txt
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 为什么要重写 ​

很多命令是冗余的,重写可以把「过程」压缩成「结果」:

txt
重写前:SET a 1; SET a 2; SET a 3; INCR a
重写后:SET a 4        ← 只保留最终结果

重写不是分析旧 AOF 文件,而是读取当前内存数据,生成一个只包含最终状态的最小命令集。

2.2.2 重写原理 ​

无论哪个版本,重写的本质相同:fork 子进程读取当前内存数据,生成只包含最终状态的最小表示;重写期间主进程继续处理写命令,并把增量记入重写缓冲区,最后合并进新文件。区别在「新文件」的形态。

Redis 7.0 之前(单文件 AOF):

txt
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 触发重写 ​

bash
auto-aof-rewrite-percentage 100   # AOF 增长超过 100% 触发
auto-aof-rewrite-min-size 64mb    # 最小触发体积(AOF < 64mb 不触发重写)

手动触发:

bash
BGREWRITEAOF

2.3 文件损坏修复 ​

AOF 文件可能因为宕机、磁盘故障等原因损坏。Redis 提供了修复工具:

bash
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 命令。

txt
混合持久化文件结构
┌─────────────────┬──────────────────────┐
│  RDB 快照头部    │  后续增量 AOF 命令     │
│  (快速恢复)     │  (实时性)           │
└─────────────────┴──────────────────────┘

Redis 7.0 起的多文件 AOF 中,混合持久化体现为「RDB 格式的基础文件 + AOF 格式的增量文件」,而非单文件内的头尾拼接。

恢复时先加载 RDB 部分(快),再重放 AOF 部分(保证最新数据)。

特性RDBAOF混合
恢复速度快慢(重放命令)快(先加载 RDB)
数据安全丢快照间隔取决于 appendfsync 与故障模型取决于 appendfsync 与故障模型
文件体积小大中等
bash
aof-use-rdb-preamble yes   # 开启混合持久化(4.0 引入,5.0 起默认开启)

4. RDB vs AOF ​

维度RDBAOF
数据安全丢失快照间隔的数据取决于 appendfsync 与故障模型
恢复速度快(直接加载二进制)慢(重放命令)
文件体积小(二进制 + 压缩)大(文本命令)
fork 开销每次快照都要 forkAOF 重写也要 fork
适用场景灾备恢复、数据迁移数据完整性优先

常见起点是同时启用 RDB 和 AOF:RDB 便于备份和快速恢复,AOF 提供更细的恢复点。Redis 同时存在两类文件时优先加载 AOF;是否同时启用仍需结合 RPO、磁盘容量、fork 成本和恢复演练判断。

5. 持久化选型 ​

场景推荐方案
缓存(可丢数据)不开启持久化
数据重要、可容忍少量丢失混合持久化 + everysec
RPO 要求接近 0AOF always + 副本与备份,且已完成压测和恢复演练
灾备恢复RDB 定期备份 + AOF 提供更细的恢复点

5.1 常见坑 ​

坑说明
主节点不开持久化主从切换时新主无数据,数据丢失
只靠从节点持久化主节点宕机数据全丢,从节点可能也没同步到
恢复优先级混乱同时存在 RDB 和 AOF 时,Redis 优先用 AOF 恢复
fsync 设置过严always 显著增加写入延迟,仅在较低 RPO 经过验证后使用

主节点关闭持久化、依赖从节点持久化时,故障转移后可能恢复到更旧的数据。是否在主节点开启 RDB/AOF 应由 RPO 决定;如果不启用,必须明确接受提升副本时的恢复点,并通过副本延迟、备份和恢复演练验证该取舍。

5.2 与备份恢复的分工 ​

本章负责持久化机制、数据丢失风险与配置选型;备份副本、恢复步骤、恢复演练与升级兼容性由备份、恢复与升级章节负责。开启持久化不等同于具备灾备能力,生产部署时应把持久化选型与业务 RPO/RTO、复制拓扑和恢复演练结合评估。

Released under the MIT License.