第 6 章 · 持久化 RDB / AOF / 混合

三个交互演示:① fork + COW 写时复制 · ② AOF 三档刷盘策略对比 · ③ AOF 重写过程(双缓冲)

BGSAVE:fork 子进程 + COW 写时复制 全过程

左边是父进程(继续接收客户端写命令),右边是子进程(遍历内存写 RDB 文件)。点「下一步」单步看 6 阶段,或者点「父进程修改第 N 页」观察 COW 触发:

🟢 父进程(PID=100,主线程继续服务)

逻辑视图:16 个内存页(每页 4 KB)
客户端 QPS
0
已写入页数
0

🔴 子进程(PID=200,写 RDB)

看到的是 fork 那一刻的内存快照
已落盘页
0 / 16
物理内存占用
16 页
dump.rdb:(未生成)
共享只读页 父进程刚刚写入(触发 COW) 已 COW 复制的副本 子进程已落盘
关键观察:fork 之后父子进程共享物理内存,物理占用并未翻倍。 只有当父进程写入某一页时,OS 才把那一页复制一份给父进程,子进程仍看到旧版本(保证快照一致)。 所以 RDB 期间内存占用 = 原内存 + 已 COW 的页数。
⚠️ 如果父进程在 RDB 期间疯狂写入(极端场景),物理内存最坏可能接近翻倍,所以单实例内存建议控制在 10 GB 以内。

AOF 三档刷盘策略并排跑:always / everysec / no

客户端持续下发 SET 写命令。每条命令都会经过① 主线程内存 → ② OS page cache → ③ 物理磁盘三段。 三档策略在「什么时候 fsync」上有差异,断电时丢的就是「停在 page cache 里那部分」。

全局时钟:0.0 s

① always

每条命令立刻 fsync(),主线程同步等待磁盘 → 安全但慢
① 内存0
② page cache0
③ 磁盘(已落盘)0
已发送:0
已落盘:0
断电丢失:0
等待开始...

② everysec(默认)

主线程只 write 进 page cache,BIO 后台线程每秒 fsync 一次
① 内存0
② page cache0
③ 磁盘(已落盘)0
已发送:0
已落盘:0
断电丢失:0
等待开始...

③ no

从不主动 fsync,全权交给 OS(一般 ~30 秒一次)
① 内存0
② page cache0
③ 磁盘(已落盘)0
已发送:0
已落盘:0
断电丢失:0
等待开始...
实验玩法:点「开始模拟」让三栏并发跑(每秒约 4 条 SET);运行几秒后点「⚡ 模拟断电」,看三栏的「断电丢失」分别是多少: always 永远是 0;everysec 一般丢 0~4 条;no 可能丢一大批。 色块含义:蓝 = 主线程内存 · 黄 = page cache · 绿 = 已落盘 · 灰 = 断电丢失

AOF 重写:把冗余命令压成最小集 + 双缓冲不丢数据

左侧是原始 AOF(很多对同一 key 的反复 SET / INCR / LPUSH / DEL); 点「触发重写」后右侧子进程基于当前内存状态生成最精简命令; 重写期间主进程的新写入会同时进入两个缓冲区,结束时合并到新 AOF。

📜 原始 appendonly.aof(旧)

命令数:0 · 大小估算:0 B

📦 新 appendonly.aof(重写后)

尚未重写

🔀 重写期间的「双缓冲」机制

子进程开始重写后,主进程接收的新命令会同时写入两个缓冲区——左边保证旧 AOF 仍可用,右边等子进程结束后追加到新 AOF 末尾:

🟦 原 AOF 缓冲区(aof_buf)
→ 直接 fsync 到旧 appendonly.aof(万一重写失败仍可用)
(暂无新命令)
🟨 AOF 重写缓冲区(aof_rewrite_buf)
→ 子进程结束后追加到新 AOF 末尾,再 rename 替换
(重写未触发)
重写状态
未开始
原文件命令数
0
重写后命令数
-
压缩率
-
核心结论:重写不是「读旧文件压缩」,而是子进程基于当前内存 dump 出最小命令集。 重写期间主进程双写新命令;子进程一结束,把右边那份重写缓冲区追加到新文件末尾, 然后 rename(临时, appendonly.aof) 原子替换。
Redis 7+ 的 multi-part AOF 把这套机制升级为「base.rdb + incr.aof + manifest」三件套,思想一致但更优雅。