Skip to content

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

提出问题

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

看起来简单,但实际工程里会遇到几个关键问题:RDB 的 fork 子进程在 10GB 内存上会阻塞多久?AOF 的 appendfsync everysec 真的最多丢 1 秒数据吗?fork 后的 copy-on-write 怎么工作的?混合持久化恢复时 RDB 段和 AOF 段怎么衔接的?带着这些问题往下看。

分析问题

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,内核缓冲区里的数据还没刷盘就丢了,最多丢 1 秒。OS crash 或断电同样最多丢 1 秒,因为上一次 fsync 之后的数据最多积累 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 工程化搞透。
粤ICP备2026104257号-1