第 9 篇 的快照在 fork 时刻冻结数据;AOF(Append Only File)走另一条路:把改变数据的命令按 RESP 追加到日志,重启时伪客户端重放。粒度更细,可配置更接近「每条写都落盘」,但文件膨胀与 rewrite 尖刺是代价。
本文锚定 Redis OSS 7.4.x
aof.c 的多段清单(manifest)模型与
flushAppendOnlyFile()。丢失窗口与 fsync
语义必须和 fsync
失败的真相 一起读——appendfsync always
下写错误/ fsync 错误会
exit(1),不是重试能救。
本文是「Redis / 缓存内核」系列第 10 篇(共 16 篇)。→ 系列目录
版本锚定:Redis OSS 7.4 / 8.x;多段 AOF(base + incr)为默认组织方式。
一、问题:命令日志记录「增量」,不是状态
AOF 的思想与数据库 WAL / 逻辑日志 同构:先记录操作,崩溃后通过重放恢复。与 RDB 对比:
| 维度 | RDB | AOF |
|---|---|---|
| 内容 | 序列化对象 | SET/HSET/… RESP 文本 |
| 体积 | 紧凑 | 随时间膨胀 |
| 恢复 | rdbLoad |
重放命令(可能较慢) |
| 丢失窗口 | 两次 BGSAVE | appendfsync 决定 |
7.4 默认不把全部历史写进单个
appendonly.aof,而用 AOF
manifest 管理:
- BASE:rewrite 产生的基础文件(可为 RDB 二进制,见第 11 篇)
- INCR:增量 AOF,记录 BASE 之后的命令
- HISTORY:被替换的旧 BASE,待删除
命名示例(aof.c
注释):appendonly.aof.1.base.rdb、appendonly.aof.1.incr.aof。
二、写入路径:feedAppendOnlyFile → flushAppendOnlyFile
写命令成功修改内存后,feedAppendOnlyFile(dictid, argv, argc)
把命令序列化进
server.aof_buf(catAppendOnlyGenericCommand
生成 *argc\r\n$len\r\n... 格式)。DB
切换时自动插入 SELECT。
主循环在 beforeSleep 等时机调用
flushAppendOnlyFile(force):
write(aof_fd, aof_buf)进内核页缓存;- 按
appendfsync决定是否/何时fsync。
flowchart LR
cmd["write command OK"]
feed["feedAppendOnlyFile"]
buf["server.aof_buf"]
flush["flushAppendOnlyFile"]
disk["page cache → disk"]
cmd --> feed --> buf --> flush --> disk
aof_rewrite_incremental_fsync 控制 rewrite
子进程阶段性 fsync,降低单次 fsync 体积。
2.1 Manifest 与多文件协同
7.4 启动时 aofLoadManifestFromDisk() 读取
appendonly.aof.manifest 索引,确定当前
BASE 与有序 INCR
列表。在线写入只追加到最新 INCR;rewrite
成功后原子切换 BASE、把旧 BASE 标为 HISTORY
并异步删除。父进程在子进程 rewrite 期间仍向旧 INCR
追加,并把新命令同时写入
aof_rewrite_buf_blocks,防止 fork
窗口丢命令——这是混合持久化能无缝衔接的工程关键(与 第
11 篇 的 preamble 切换共用同一套 manifest 逻辑)。
三、appendfsync 三档与丢失窗口
| 模式 | 行为 | 典型丢失窗口 | 延迟 |
|---|---|---|---|
always |
每次 flush 后
redis_fsync(Linux 上常为
fdatasync) |
已应答复客户端的写,若 fsync 成功则≈0;失败则进程退出 | 最高 |
everysec |
每秒后台
aof_background_fsync;写入可推迟最多约 2s
若上次 fsync 未完成 |
最多约 1 秒 已确认写(官方文档口径);灾难断电可能多丢 fsync 未完成缓冲 | 默认折中 |
no |
由 OS 刷盘 | 可达数十秒或更多 | 最低 |
3.1 everysec 细节
flushAppendOnlyFile 在
AOF_FSYNC_EVERYSEC 下:
- 若上次后台 fsync 仍在进行,可推迟写最多 2
秒(
aof_flush_postponed_start),超时则强制写并记aof_delayed_fsync。 - 即使
aof_buf为空,也可能因「上次增量未 fsync」而触发try_fsync——避免静默丢最后一秒数据。
这与 数据完整性
中「write 成功 ≠
持久化」一致:everysec
承诺的是秒级,不是事务级。
3.2 always 的失败语义
aof.c 明确:若
appendfsync always 且 AOF write 或
fsync 失败,调用
exit(1)——因为回复可能已发给客户端,无法回滚内存状态。这与
PostgreSQL 在 fsync 失败后 PANIC 同族(见 fsyncgate):不能靠重试
fsync 假装成功。
everysec / no 下写失败会设
aof_last_write_status = C_ERR
并暂停接受写,保留 buffer 待恢复。
四、AOF rewrite:压缩日志
命令日志对同一 key 多次 SET
会留下多条记录;rewrite
在子进程遍历当前内存,生成等价的最小命令集(或 RDB
preamble,第 11 篇)。
rewriteAppendOnlyFileBackground():
fork子进程;- 子进程
rewriteAppendOnlyFile或rewriteAppendOnlyFileRio写临时文件; - 父进程继续把新命令追加到 旧 INCR + rewrite
缓冲(
aof_rewrite_buf_blocks); - 子进程结束后,父进程把累积增量追加到新 BASE,切换 manifest。
触发:
BGREWRITEAOF手动;auto-aof-rewrite-percentage/min-size自动;- 子进程失败计数
aof_rewrites_consecutive_failures。
尖刺:rewrite 与 BGSAVE 一样占用
fork 与磁盘
IO;no-appendfsync-on-rewrite yes
可在子进程期间跳过 fsync
以换吞吐(牺牲该窗口内最高耐久)。
rewriteAppendOnlyFileRio 对每 key 生成
SET/RPUSH/… 再跟
PEXPIREAT,并在 fork 子进程中对已 dump 对象
dismissObject() 减 COW——与 RDB
子进程优化同思路。
五、加载与校验
启动时 loadAppendOnlyFile 按 manifest
顺序:先 BASE 再 INCR。纯 AOF BASE 重放 RESP;RDB 格式 BASE
走 rdbLoad(混合持久化)。
aof-load-truncated yes
允许截断损坏尾部后启动——显式承认可能丢尾部命令。redis-check-aof
工具做离线修复。
aof-timestamp-enabled 时 AOF 可带
#TS: 行标注 Unix
秒,便于审计命令边界,不改变重放语义。
六、学术谱系与争论
谱系:ARIES WAL → Redis antirez
AOF(2010 年代)→ 4.x fsync 策略固化 → 7.x 多段
manifest + 可选 RDB preamble。
争论:
- always vs everysec:always 贴近「存储」语义;everysec 是缓存/会话主流,但断电丢 1 秒是设计内建,不是小概率事故。
- rewrite 频率:过小文件浪费 fd 与
manifest 切换;过大文件 rewrite
时间长、恢复慢。
auto-aof-rewrite-percentage100 表示翻倍触发,需结合写入速率调。 - AOF
与复制:
WAIT/WAITAOF把复制与本地 fsync 绑定;AOF offset 与fsynced_reploff_pending在模块NO_AOF路径有边角(flushAppendOnlyFile注释)。
开放问题:NVMe + 高 hz 下
everysec 的尾延迟与「1 秒丢失窗口」在 kube 强制
kill 场景是否可接受,需业务 RPO 定义,非 Redis
单点能答。
七、常见误解
7.1 「AOF 开着就不会丢数据」
只有 appendfsync always 且 fsync
成功才接近「每条写持久」;everysec 明确允许丢约
1 秒。
7.2 「rewrite 会丢数据」
不会丢逻辑状态;rewrite 生成与当前内存等价的 BASE。fork 窗口内的增量由父进程缓冲并在结束后追加。
7.3 「fsync 失败重试就好」
在 always 模式下错误;见 fsyncgate。Redis 选择退出或停写,而非假装成功。
7.4 「AOF 比 RDB 恢复总是更慢」
纯命令 AOF 常更慢;RDB preamble 的 BASE 使恢复接近 RDB 速度(第 11 篇)。
八、小结
三句话小结
- AOF 把变更命令追加到 manifest 管理的 BASE/INCR 文件,恢复靠重放或加载 RDB 型 BASE。
appendfsync在 always / everysec / no 之间划分耐久与延迟;everysec 的丢失窗口约 1 秒是协议一部分。- rewrite fork 子进程压缩日志;与 BGSAVE 互斥子进程槽,并共享 COW/fork 代价。
上一篇:RDB 快照
下一篇:混合持久化与崩溃语义
参考资料
- Redis OSS 7.4
源码:
src/aof.c(feedAppendOnlyFile、flushAppendOnlyFile、rewriteAppendOnlyFileBackground、getNewBaseFileNameAndMarkPreAsHistory)、src/config.c(appendfsync、aof-use-rdb-preamble)。 - Redis Documentation v7.x, Persistence、AOF、fsync policy。
- fsync 失败的真相、数据完整性。
- 系列 index、第 11 篇混合持久化。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Redis / 缓存内核】混合持久化与崩溃语义
说明 Redis 7.4 默认开启的 RDB preamble + INCR AOF 如何组合恢复路径,结合 fsyncgate 与数据完整性给出 appendfsync 与混合模式的配置决策树及耐久争论边界。
【Redis / 缓存内核】RDB:fork COW · BGSAVE · 快照格式
从 rdb.c 拆解 SAVE/BGSAVE 的 fork 写时复制语义、子进程快照路径,以及 REDIS 魔数与 opcode 锚定的 RDB 格式边界。
【Redis / 缓存内核】事件循环 · 对象编码 · 淘汰 · RDB/AOF · Cluster
补齐内存 KV / 缓存服务器内核层:从 ae 事件循环与 redisObject 多态编码,到 TTL/maxmemory 近似淘汰、RDB/AOF 持久化语义,再到复制、Cluster hash slot 与 Memcached 对照。
【Redis / 缓存内核】TTL 与过期:expires dict · 惰性/主动删除 · lazyfree
拆解 Redis 7.4 的 TTL 机制:expires 辅助表、lookup 路径惰性删除、activeExpireCycle 自适应采样,以及 lazyfree 异步释放大对象对延迟的影响。