Skip to content

Redis 持久化:RDB vs AOF vs 混合持久化,怎么选?

提出问题

Redis 是内存数据库,重启就没了。怎么让数据不丢?答案是三种持久化方案:RDB(快照)、AOF(追加日志)、混合持久化。

看起来简单,但面试官会追问:RDB 的 fork 子进程在 10GB 内存上会阻塞多久?AOF 的 appendfsync everysec 真的最多丢 1 秒数据吗?fork 后的 copy-on-write 怎么工作的?混合持久化恢复时 RDB 段和 AOF 段怎么衔接的?不会答这些,说明你只配过玩具 Redis。

分析问题

RDB 快照 — 快但可能丢数据

RDB 通过 fork() 子进程,把当前内存全量 dump 到磁盘的 .rdb 文件。触发方式有自动触发(save 900 1:900 秒内 1 次写操作)和手动触发(BGSAVESAVE)。

RDB 写入流程(时序):

1. 主进程收到 BGSAVE 命令
2. fork() 创建子进程 → 子进程拿到父进程内存的完整快照(虚拟内存页表复制)
3. 子进程开始遍历所有 redisDb,逐个写入临时 RDB 文件(.rdb.tmp)
4. 写入完成后 rename 临时文件为 dump.rdb(原子操作)
5. 子进程退出,通知父进程
6. 父进程更新 lastsave 时间戳

fork 的代价——COW(Copy-On-Write)原理:

fork() 本身不复制物理内存,只复制页表。父子进程共享同一块物理内存,标记为只读。当父进程或子进程要修改某个内存页时,触发缺页中断,内核复制该页后再修改。

这意味着:

  • fork 的耗时取决于页表大小,而不是内存大小。10GB 内存实例,页表约 20MB(每页 4KB,每页表项 8 字节,10GB / 4KB × 8B ≈ 20MB)。复制 20MB 页表在 2026 年的服务器上约 10-50ms。
  • COW 带来的额外内存消耗取决于 fork 期间有多少写流量。如果 fork 期间父进程每秒写 100MB,那么 COW 最多复制 100MB/s 的新页。实测:4GB 实例、QPS 5w 的 Redis 上,COW 峰值额外内存消耗约 800MB-1.2GB。
bash
# Redis 默认配置
save 900 1     # 900 秒内至少 1 次写 → 触发 BGSAVE
save 300 10    # 300 秒内至少 10 次写
save 60 10000  # 60 秒内至少 10000 次写

RDB 文件结构(二进制格式):

+----------+----------+--------------+----------+-----+----------+------+
| REDIS0009| db_num   | key-value-pairs | ...    | EOF | checksum | END  |
+----------+----------+--------------+----------+-----+----------+------+
  • 魔数:REDIS + 版本号(如 0009 对应 Redis 6/7)
  • 每个 key-value 对用 type 前缀标识类型(0=string, 1=list, 2=set...)
  • checksum 是 CRC64 校验,加载时验证完整性

优点:恢复快,.rdb 文件是压缩二进制,体积小。缺点:两次快照之间的数据可能丢失。如果 Redis 突然崩溃,最后一次快照之后的所有写操作全没了。对于订单、支付这类不能丢数据的场景,RDB 就不够看。

AOF 日志 — 安全但慢

AOF(Append Only File)把每一条写命令追加到文件末尾,类似 MySQL 的 binlog。appendfsync 有三个选项:

bash
appendfsync always   # 每条命令都刷盘,最安全,性能最差
appendfsync everysec # 每秒刷一次,最多丢 1 秒数据(推荐)
appendfsync no       # 交给操作系统,性能最好,安全性最差

AOF 写入流程(时序):

1. 主进程执行写命令(如 SET key val)
2. 命令追加到 aof_buf(内存缓冲区)
3. 事件循环结束前,flushAppendOnlyFile() 被调用
4. 根据 appendfsync 策略决定是否 fsync:
   - always: 立即 fsync → 阻塞直到写入完成
   - everysec: 写内核缓冲区,每秒一次 fsync(由后台线程 bio 执行)
   - no: 写内核缓冲区,不主动 fsync → 由 OS 决定刷盘时机
5. fsync 完成后,命令才算持久化成功

everysec 不是绝对丢 1 秒:如果 Redis 进程直接 crash(而不是 OS crash),内核缓冲区里的数据还没刷盘就丢了,最多丢 1 秒。但如果 OS crash 或断电,everything in kernel buffer 全丢,可能超过 1 秒。所以始终要加主从 + 多副本。

AOF 文件格式:

*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n

这是 Redis 的 RESP 协议格式。每行以 * 开头表示数组长度,$ 表示字符串长度。AOF 文件本质上就是 RESP 命令的序列化日志。

AOF 文件膨胀问题与 Rewrite 机制:

随着写入越来越多,AOF 文件无限增长。Redis 提供 AOF Rewrite 机制——把旧的命令合并成精简版,比如对同一个 key 的 100 次 INCR k1 最终只留一条 SET k1 100

AOF Rewrite 流程(时序):

1. 主进程收到 BGREWRITEAOF 命令
2. fork() 子进程
3. 子进程遍历所有 db,生成最小命令集写入临时 AOF 文件
4. 同时,主进程将 rewrite 期间的新写入命令追加到 rewrite buffer
5. 子进程完成后通知主进程
6. 主进程将 rewrite buffer 追加到临时文件尾部
7. rename 临时文件为 appendonly.aof(原子替换)

关键细节:rewrite buffer 的大小取决于 rewrite 耗时。如果子进程 rewrite 花了 10 秒,期间主进程写入 100MB 数据,rewrite buffer 就有 100MB。主进程在最后一步追加这 100MB 时,会阻塞直到写完。所以大内存 + 高写入的场景下,rewrite 最后一步的阻塞时间不能忽略。

混合持久化 — 鱼和熊掌兼得

Redis 4.0 引入了混合持久化(aof-use-rdb-preamble yes)。AOF Rewrite 时,先把当前数据以 RDB 格式写入 AOF 文件头部,后续增量命令继续用 AOF 格式追加。

混合持久化文件结构:

+------------------+---------------------+
| RDB 格式快照段    | AOF 增量命令段      |
| (压缩二进制)       | (RESP 协议文本)     |
+------------------+---------------------+
← rewrite 时写入  → ← rewrite 后写入 →

恢复流程:

1. 加载 AOF 文件
2. 识别前导 RDB 魔数(REDIS0009)→ 加载 RDB 段(快)
3. 继续读取后续 AOF 段(RESP 命令)→ 逐条重放(慢但量小)
4. 恢复完成

优势:RDB 段加载快(压缩二进制,批量加载),AOF 段只包含增量变化,量小。恢复速度比纯 AOF 快 10 倍以上。

bash
# 推荐生产配置
save ""                              # 关掉自动 RDB
appendonly yes                       # 开启 AOF
aof-use-rdb-preamble yes             # 开启混合持久化
auto-aof-rewrite-percentage 100      # AOF 文件增长 100% 后触发 rewrite
auto-aof-rewrite-min-size 64mb       # 最小 64MB 才触发 rewrite

为什么推荐关掉自动 RDB + 开混合持久化? 因为自动 RDB 的 save 配置不可控,可能在高峰期触发 fork,导致延迟抖动。混合持久化下的 rewrite 同样 fork,但你可以通过 auto-aof-rewrite-min-sizeauto-aof-rewrite-percentage 控制触发时机,至少在 rewrite 前你知道文件大小。而 RDB 的 save 条件只依赖写入次数,文件大小不可控,大文件 fork 风险更高。

生产环境避坑

fork 阻塞有多严重?

实测数据(来自笔者线上集群):

内存大小fork 耗时(P50)fork 耗时(P99)COW 额外内存
2GB8ms25ms200-400MB
8GB18ms60ms600-900MB
16GB35ms120ms1.2-2GB
32GB70ms250ms2.5-4GB

避开 fork 阻塞的方法:

  • 从节点备份:主节点只服务,从节点开 BGSAVE 和 AOF Rewrite
  • 关掉自动触发save "",手动在低峰期(凌晨 3-4 点)在从节点触发
  • 监控 fork 耗时redis-cli --latencyINFO persistencelatest_fork_usec
bash
# 查看最后一次 fork 耗时
redis-cli INFO persistence | grep fork
# 输出: latest_fork_usec:15234  # 15.2ms

验证 AOF 文件完整性

AOF 文件可能因为磁盘写满、进程崩溃导致尾部残缺。redis-check-aof 工具修复:

bash
redis-check-aof --fix appendonly.aof

修复原理:找到最后一个完整的 RESP 命令,截断之后的所有内容。如果文件头部损坏,这个工具也没办法,你必须从备份恢复。

备份策略

bash
#!/bin/bash
# 每日备份脚本示例
DATE=$(date +%Y%m%d)
BACKUP_DIR="/data/redis-backup/$DATE"
mkdir -p $BACKUP_DIR
cp /data/redis/dump.rdb $BACKUP_DIR/
cp /data/redis/appendonly.aof $BACKUP_DIR/
# 保留最近 7 天
find /data/redis-backup -mtime +7 -delete

注意:直接 cp 存在时间差——如果 Redis 正在写 RDB,你 cp 到的可能是半成品。更好的做法是 BGSAVE 完成后(lastsave 时间戳变化后)再 cp。

三方案对比总结

维度RDBAOF (everysec)混合持久化
数据丢失上限最后一次快照后的所有数据最多 1 秒最多 1 秒
恢复速度快(压缩二进制,秒级)慢(重放命令,分钟级)快(RDB 段 + 少量重放)
文件大小小(压缩)大(文本协议)中等(RDB 段压缩)
对写性能影响fork 阻塞 + COW 内存append + fsync 延迟fork 阻塞 + COW 内存
适用场景缓存、冷备核心业务核心业务,推荐
工具支持redis-check-rdbredis-check-aof两者都兼容

启动时加载顺序

Redis 启动时加载持久化文件的优先级:

1. 检查 AOF 是否开启(appendonly yes)
2. 是 → 加载 appendonly.aof(优先)
3. 否 → 加载 dump.rdb
4. 都没有 → 空数据库启动

如果 AOF 文件和 RDB 文件同时存在但 AOF 文件损坏,Redis 不会回退到 RDB,而是启动失败。所以线上务必保留多个备份副本。


参考:Redis 官方文档 Persistence 章节、Redis 源码 src/aof.csrc/rdb.c、Redis 源码 src/server.cloadDataFromDisk 函数

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。