Theory · Redis · Persistence
fork 快照 + 追加日志 + 两全其美的混合 —— 数据安全、性能、恢复速度的三角权衡
fork + 写时复制的二进制快照:文件紧凑、恢复快,代价是快照间隙的数据丢失窗口
写后命令日志:everysec 最多丢约 2 秒;7.0 Multi-Part AOF 用 base+incr+manifest 根治重写顽疾
aof-use-rdb-preamble:重写时 RDB 打头、增量 AOF 收尾——体积与恢复速度兼得,生产默认
Why Persist
内存数据库,进程退出数据即消失;持久化文件让重启后能重建数据集。官方文档开门见山:没有持久化的 Redis 有时被当作纯缓存,但大多数场景需要它。
主从第一次同步 / 断线过久无法增量复制时,主库执行的正是 bgsave 生成 RDB 发给从库全量加载。且官方明确:RDB 在副本上支持重启/故障转移后的部分重同步(RDB 里存了 replication id/offset)——详见 replication deck。
| 与持久化联动的机制 | 关系 |
|---|---|
| fork / 写时复制(COW) | RDB 与 AOF 重写都靠 fork 子进程做重活——持久化的性能问题本质是 fork 与 COW 的内存问题(第 4/6 页) |
| bio 后台线程 | AOF 的 everysec fsync 跑在 BIO_AOF_FSYNC 线程(thread-model deck),磁盘慢不直接卡主线程 |
| 内存淘汰(maxmemory) | 复制/AOF 缓冲不计入淘汰判定(mem_not_counted_for_evict)——官方建议给副本/持久化实例预留内存余量 |
| 主从复制互斥 | Redis ≥2.4 保证 BGSAVE 与 AOF 重写不同时跑:两个重型磁盘 IO 子进程不同时上 |
Overview
二进制、单文件、时间点一致。像"存档":恢复 = 直接读档,速度快。存档之间发生的事不在档里——快照间隙宕机就丢这段。dump.rdb 由 save 点控制自动生成。
像"操作账本":重启 = 从头重放账本重建状态。记录全 → 最多丢一秒(everysec);但账本越记越长,需要定期"重写"压缩成当前状态的最短命令集。
| 官方四种选项 | 说明 |
|---|---|
| RDB | 定时快照;适合备份/灾备/快速恢复 |
| AOF | 追加命令日志,更耐久;官方提醒:不建议只开 AOF——定期 RDB 快照对备份、快速重启、防 AOF 引擎 bug 都有价值 |
| 无持久化 | 纯缓存场景(save "" + appendonly no) |
| RDB + AOF 同开 | 想要 PostgreSQL 级数据安全就都开;重启时 AOF 优先加载(保证最完整) |
RDB · SAVE / BGSAVE
RDB Config · Pros & Cons
# redis.conf(8.x 默认三个 save 点) save 3600 1 # 1 小时内 ≥1 次修改 save 300 100 # 5 分钟内 ≥100 次修改 save 60 10000 # 1 分钟内 ≥10000 次修改 # 相关参数: dbfilename dump.rdb # 文件名 dir ./ # 存放目录 rdbcompression yes # LZF 压缩 rdbchecksum yes # CRC64 校验 save "" # 完全关闭自动快照
| 触发方式 | 说明 |
|---|---|
| save 点(自动) | 满足"N 秒 M 次修改"即 BGSAVE(serverCron 检查 dirty 计数) |
| 手动 | BGSAVE 异步 / SAVE 同步阻塞;SHUTDOWN 且有 save 点时关闭前也会保存(kill -9 除外) |
| 主从 | 全量同步时主库执行 BGSAVE 生成 RDB 发送给从库 |
| 互斥 | AOF 重写进行中时 BGSAVE 被拒绝/排队(≥2.4) |
| RDB | 官方口径(persistence 文档) |
|---|---|
| 优点 | 单文件紧凑、适合备份/异地灾备/S3 归档;父进程除 fork 外不做任何磁盘 IO;大 dataset 重启比 AOF 快;副本重启/切换后可部分重同步 |
| 缺点 | 快照间隙宕机丢"最近几分钟"数据;大 dataset 频繁 fork 耗时可观(毫秒级,极端可到 1 秒级),且 COW 放大内存 |
fork / COW in Production
fork 复制的是页表不是内存:10GB 实例 ≈ 268 万个 4KB 页 ≈ 21MB 页表需要内核复制。官方文档:数据集大且 CPU 不强时,fork 可能让服务停摆"几毫秒甚至一秒"。实例越大、fork 越贵——这就是"单实例别超 10GB"的依据之一。
BGSAVE 期间父进程每个写操作都可能触发页复制——写入热点越散,放大越明显,最坏逼近数据集翻倍,可能触发 OOM killer。另一个大坑:透明大页 THP 把页从 4KB 变 2MB,COW 粒度放大 512 倍,一次写复制 2MB——Redis 官方启动日志直接建议 echo never > /sys/kernel/mm/transparent_hugepage/enabled。
| 手段 | 作用 |
|---|---|
| 关 THP(madvise/never) | COW 粒度回到 4KB,fork 后写放大可控——生产实例标配 |
| 预留内存余量 | 机器内存 ≥ maxmemory × 2 再开持久化(COW + 复制缓冲都要空间);云上监控 RSS 而非 used_memory |
| 低峰重写/快照 | 调整 save 点与 auto-aof-rewrite 时机;bigkeys 写入热点集中也能减少 COW 页数 |
| 控制实例规模 | 单实例数据集上限(官方建议参考 10–20GB 量级),或上集群分片——fork 时长是线性于页表规模的 |
AOF · Write-after-log
命令先执行成功、再追加到 AOF(传统数据库 WAL 是先写日志)。好处:① 不阻塞当前写操作;② 日志里只有成功执行的命令,不会记录语法错误的命令——重启重放天然安全。代价:命令执行完到真正落盘之间有一个丢失窗口(下一页的 fsync 策略就在管这个窗口)。
命令执行完 → 追加进 aof_buf(redisServer.aof_buf)→ 每轮事件循环 beforeSleep 统一 write() 到内核页缓存 → fsync() 何时执行由 appendfsync 决定(everysec 时交给 BIO_AOF_FSYNC 后台线程)。write 不等于落盘:write 只进 OS 缓冲,fsync 才到磁盘。
| AOF 文件内容 | RESP 文本协议记录(人可读) |
|---|---|
| 示例 | *3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n —— 与客户端协议同格式,7.0 起 base 文件甚至可以是 RDB 格式(混合) |
| 记录什么命令 | 所有"改变了数据集"的写命令(含 DEL/EXPIRE/UNLINK);7.0 起集合/哈希被删空的隐式删除(如 HDEL 最后一个 field)会以显式 DEL 传播进 AOF 与从库——保证主从不出现"幽灵空 key" |
| 恢复 | 启动时建伪客户端(fake client)从头重放——没有 Lua/事务上下文,纯命令重放 |
appendfsync always / everysec / no
| 策略 | 机制 | 丢失窗口 | 性能口径(官方) |
|---|---|---|---|
always | 每批命令追加后立即 fsync(多客户端/管道的命令合并成一次 write + 一次 fsync,组提交) | ≈ 0(最多丢"正在执行未追加"的那条) | Very very slow, very safe(官方原话) |
everysec(默认) | write 每轮循环都做;fsync 交给后台线程每秒一次;上次 fsync 未完成时推迟 flush,推迟超 2 秒才强制执行 | 官方口径 1 秒;机制最坏 ≈ 2 秒(推迟写的上限) | fast enough(2.4 起接近快照性能) |
no | 只 write 不主动 fsync,节奏交给操作系统(Linux 通常 30 秒刷一次) | 不确定(可能 30s 级) | 最快也最不安全(the faster and less safe) |
AOF Rewrite · BGREWRITEAOF
INCR counter 执行 100 次,AOF 里 100 条命令只对应内存里 1 个 key——99 条是废账。重写 = fork 子进程按当前内存状态生成"重建数据集的最短命令集"。官方保证重写安全:期间继续追加旧文件,新文件就绪后才切换。
子进程写新 AOF 期间,父进程把新写命令同时存进内存缓冲 aof_rewrite_buf,结束后补写进新文件。官方文档列的代价:①重写期有写入时 AOF 可占用大量内存(缓冲无上限,实战能把实例打爆);②写命令落盘两次(旧文件一次、缓冲一次);③结束时父进程可能被补写 + fsync 冻结。
| 重写的自动触发 | 说明 |
|---|---|
auto-aof-rewrite-percentage 100 | 体积比上次重写后增长 ≥100% 就触发 |
auto-aof-rewrite-min-size 64mb | 且当前体积 ≥64MB(两个条件同时满足) |
| 手动 | BGREWRITEAOF;与 BGSAVE 互斥排队(≥2.4) |
Redis 7.0 · Multi-Part AOF
Hybrid · aof-use-rdb-preamble
aof-use-rdb-preamble(4.0 引入,5.0 起默认 yes):重写时 BASE 文件用 RDB 二进制格式写全量快照,之后的增量继续以 RESP 命令追加进 INCR 文件。文件头 = 高密度 RDB;尾巴 = 少量 AOF 文本。
效果:重写后的"账本"从 100 万条命令变成 1 个 RDB 快照 + 少量增量——AOF 体积接近 RDB、恢复速度接近 RDB、耐久性仍是 AOF 级。
启动时读 manifest → 先加载 BASE(识别到 RDB 魔数走 RDB 解析,日志 "Reading RDB preamble from AOF file")→ 再顺序重放各 INCR 的命令尾部。两段都完成才代表恢复成功;INCR 尾部截断可按 aof-load-truncated 容忍。
| 追问 | 答案 |
|---|---|
| 为什么混合快? | RDB 是紧凑二进制全量加载,比逐条解释执行百万条命令快一个量级;增量部分通常很小 |
| 为什么混合小? | 快照二进制密度远高于文本命令;老版本全 AOF 格式重写后依然要存"重建用的最短命令",混合后直接存状态本身 |
| 关掉它图什么? | 几乎不图——只有需要"纯文本 AOF 可人工审计/编辑"时才设 no(比如手工删掉误操作命令再恢复) |
| 版本口径 | 4.0 引入参数、5.0 起默认 yes;7.0 起混合形态自然演化为 Multi-Part 的 BASE(RDB)+INCR(AOF) |
Operations Cheat Sheet
尾部截断(宕机写一半):默认 aof-load-truncated yes 直接丢弃残缺尾命令照常启动;要严格就设 no。中部损坏(非法字节):启动报 Bad file format 拒绝启动——先备份,跑 redis-check-aof --fix(7.0+ 支持 Multi-Part,对 manifest 整组修);注意 fix 会把损坏点到末尾全部丢弃,可能大量丢数据。
RDB:任意时刻拷贝 dump.rdb 都安全(rename 原子、生成后只读)。AOF:拷贝整个 appendonlydir;但重写进行中拷贝可能不一致——官方流程:CONFIG SET auto-aof-rewrite-percentage 0 暂停自动重写 → INFO 确认 aof_rewrite_in_progress=0 → 拷目录 → 恢复配置。
| 参数 | 默认 | 一句话 |
|---|---|---|
appendonly | no | AOF 总开关(7.0 起打开即自动生成 appendonlydir 三件套) |
appendfsync | everysec | 落盘策略(第 8 页) |
no-appendfsync-on-rewrite | no | 设 yes = 重写/BGSAVE 期间不 fsync(避免磁盘 IO 抖动,代价是该窗口丢数据风险升高) |
aof-use-rdb-preamble | yes | 混合持久化开关 |
aof-load-truncated | yes | 容忍尾部截断 |
auto-aof-rewrite-percentage / min-size | 100 / 64mb | 自动重写触发(翻倍 + 起步量) |
aof-timestamp-enabled | no | 在 AOF 中记录时间戳(用于按时间点恢复,有开销) |
RDB vs AOF vs Hybrid
| 维度 | RDB | AOF(everysec) | 混合(默认形态) |
|---|---|---|---|
| 数据安全 | 丢分钟级(save 点间隙) | 丢 1~2 秒 | 丢 1~2 秒(同 AOF) |
| 写性能影响 | 几乎无(fork 之外零磁盘 IO) | write 常态开销小;fsync 在后台;重写要 fork | 同 AOF;重写频率更低(体积小 → 增长 100% 更慢) |
| 文件体积 | 最小(二进制快照) | 大(文本命令 + 需重写压缩) | 接近 RDB |
| 恢复速度 | 快 | 慢(逐条重放) | 快(RDB 头 + 少量增量) |
| 可读/可审计 | 无(二进制) | RESP 文本可读可编辑 | 头不可读、尾可读 |
| 兼容性/风险点 | 版本升级兼容性好 | 重写开销、缓冲(<7.0)风险 | 依赖 RDB 解析正确性;旧版本 Redis 不识别 RDB 头 |
要 PostgreSQL 级安全 → 两个都开;能忍分钟级丢失 → RDB 单开即可;官方不鼓励 AOF 单开——定期 RDB 对备份、快速重启、防 AOF 引擎 bug 都是保险。
缓存:RDB(甚至无持久化)+ 从库兜底;数据重要:AOF everysec + 混合 + RDB 双开,机器内存按 maxmemory×2 预留,关 THP;配合从库与哨兵实现"持久化 + 复制"双保险(replication deck)。
Interview QA · 1/2
SAVE 由主线程同步写 RDB,全程阻塞;BGSAVE fork 子进程写临时文件后 rename 原子替换,父进程只在 fork 瞬间阻塞(页表复制)。save 点触发、主从全量同步走的都是 BGSAVE。
能。fork 后父子共享物理页;父进程写某页时内核 COW 复制该页给父进程,子进程始终读到 fork 瞬间的旧数据——快照是 t0 时刻的一致视图。子进程越快写完,COW 放大越小。
会,阻塞时长 = 内核复制页表的时间,正比于数据集页数:10GB ≈ 21MB 页表。官方口径:数据集大 + CPU 弱时可停毫秒级甚至 1 秒。配套手段:关 THP(否则 COW 粒度 2MB,放大 512 倍)、控实例规模。
save N M = N 秒内至少 M 次修改则触发 BGSAVE。8.x 默认三个:3600 秒 1 次、300 秒 100 次、60 秒 10000 次。另:SIGTERM 正常关闭(有 save 点)也会保存;save "" 全关;AOF 重写进行中 BGSAVE 排队。
优:单文件紧凑适合备份灾备、父进程除 fork 不做磁盘 IO、大 dataset 恢复快且副本重启可部分重同步。缺:save 点间隙宕机丢分钟级数据、fork + COW 有瞬时阻塞与内存放大。
BGSAVE 期间父进程写入热点的每个涉及页都可能被复制,最坏(全部页被写)内存逼近翻倍,可能触发 OOM killer。防:机器内存 ≥ maxmemory×2、监控 RSS、关 THP、低峰触发快照、控制单实例规模。
kill -9 走不到任何持久化钩子:丢到"上一次 save 点/AOF 最后一次 fsync"为止。SIGTERM(redis-cli shutdown)触发时若有 save 点会先 BGSAVE 再退出(save "" 关闭则不保存)。
两个用途:全量同步时主库 bgsave 生成 RDB 发给从库加载;RDB 文件里存有 replication id/offset,副本重启或 failover 后可借此做部分重同步(官方:RDB supports partial resynchronizations)。
Interview QA · 2/2
与 WAL 相反:命令执行成功才追加。好处:不阻塞写操作、日志里只有成功命令,重放安全;代价:执行到落盘之间存在丢失窗口——由 fsync 策略控制(write 每轮做,fsync 按 appendfsync)。
fsync 在 BIO_AOF_FSYNC 后台线程执行;上次 fsync 未完成时,主线程推迟本轮 write 并记下推迟起点(aof_flush_postponed_start),推迟满 2 秒才强制执行。因此 aof_buf 滞留命令最多约 2 秒——官方口径"丢 1 秒",机制上界 2 秒。
重写 = fork 子进程按当前内存生成"重建数据集的最短命令序列",等效于用状态替换历史。不能滚动删旧文件:日志是整体有序的命令流,按行删无法保证重放语义;且压缩的核心是"按 key 合并",本质与快照同构——所以复用 fork/COW。
解决 <7.0 重写的三宗罪:内存缓冲翻倍、命令写两遍、结尾补写冻结。现在重写期父进程直接打开新 INCR 文件写盘,子进程生成新 BASE,完成后临时 manifest 原子 rename 生效,旧文件标 HISTORY 清理——任意时刻目录都是一致状态。
aof-use-rdb-preamble:重写时 BASE 用 RDB 二进制全量、增量继续 AOF 命令追加。4.0 引入、5.0 起默认 yes。体积与恢复速度接近 RDB、耐久性保持 AOF;7.0 后自然落位为 Multi-Part 的 BASE(RDB)+INCR(AOF)。
AOF 优先:官方明确"AOF 重建的数据集更完整"。流程:识别 manifest → 加载 RDB 头(若混合)→ 重放 INCR 尾。RDB 只在未开 AOF 时用于恢复。
尾部截断(写一半):默认 aof-load-truncated=yes,丢弃残尾照常启动。中部损坏(Bad file format):拒绝启动,先备份再 redis-check-aof --fix(7.0+ 支持整组 manifest);fix 从损坏点截到末尾,可能大丢数据,官方建议先手工分析偏移。
数据重要:appendonly yes + appendfsync everysec + aof-use-rdb-preamble yes + RDB save 保留(官方不鼓励 AOF 单开)。同时:机器内存 ≥ maxmemory×2(COW/缓冲)、关 THP、no-appendfsync-on-rewrite 默认 no、监控 aof_last_bgrewrite_status 与 AOF 增长速率。纯缓存:RDB 甚至不持久化。
Related · Cross-links
replication-cluster · 主从与集群(全量同步 = BGSAVE 产物;部分重同步依赖 RDB 元数据)
thread-model · 单线程模型(aof_buf / beforeSleep flush / BIO_AOF_FSYNC 的线程视角)
memory-policy · 过期删除与内存淘汰(过期键与 RDB/AOF 的交互、COW 与 maxmemory 的联动)
data-structures · 对象系统与数据结构(编码紧凑度 → RDB 体积与恢复速度)
MySQL MVCC:AOF 写后日志 ↔ MySQL redo log 先写日志,两种 WAL 取舍对照
Kafka Internals:AOF 追加日志 + 重写压缩 ↔ Kafka 分段日志 + 日志压实(log compaction)
OS · 进程线程协程:fork + 写时复制机制详解——bgsave 卡顿与内存翻倍的根源
通用线索:快照 vs 日志、顺序 IO、后台线程做重活三组思想反复出现
References
本 deck 全部结论可溯源至下列一手材料——数字与版本结论以原文为准。
| redis.io/docs/latest/operate/oss_and_stack/management/persistence/ | RDB/AOF 优缺点原文、fork 流程、appendfsync 三策略、<7.0 与 ≥7.0 重写流程、损坏修复与备份流程 |
| redis.io/blog(8.0-M03 / 8.0 GA)· 7.0 00-RELEASENOTES | Multi-Part AOF 设计(manifest 原子替换、rewrite limiting);7.0 起空集合不落日志等行为变化 |
| github.com/redis/redis · 8.2 redis.conf | 默认值核对:save 3600 1 / 300 100 / 60 10000、appendfsync everysec、aof-use-rdb-preamble yes、aof-load-truncated yes、auto-aof-rewrite 100/64mb、no-appendfsync-on-rewrite no |
| github.com/redis/redis · src/aof.c | flushAppendOnlyFile 的 write/fsync 分离、aof_flush_postponed_start 2 秒强制逻辑、feedAppendOnlyFile |
| github.com/redis/redis · src/rdb.c / src/bio.h | rdbSaveBackground、temp-<pid>.rdb 与 rename(2);BIO_AOF_FSYNC / BIO_CLOSE_FILE / BIO_LAZY_FREE 三线程 |
| redis.io/docs latency(THP)· latency-monitor | 透明大页禁用建议(COW 粒度 2MB 放大) |
| redis.io/docs eviction | 复制/AOF 缓冲不计入 maxmemory(mem_not_counted_for_evict)——持久化实例内存预留依据 |