核心目标:理解 RDB、AOF、混合持久化三种方式的机制、代价与恢复语义;掌握 fork + COW、fsync 策略、AOF 重写等关键概念;能设计符合业务丢失容忍度的持久化策略,并在文件损坏后完成修复与恢复。
前置知识 :完成 Part 1(单线程事件循环------持久化是"阻塞点"的重要来源)与 Part 3(内存结构)。
验证环境:Redis 8.10.0(cygwin 移植版,主实例 6379 用于常规实验,隔离实例 6380 用于断电/损坏等破坏性实验)、redis-py 8.1.0、Python 3.11.6、Windows 11。最后复核日期:2026-08-07。
0. 本篇问题场景:数据"没丢"只是运气
"Redis 是内存数据库,重启就清空"是常见的误读。真实情况是:Redis 默认配置下 (appendonly no)只做 RDB 快照,且快照触发频率可能很低------kill -9 或断电后,上一次快照之后写入的所有数据都会消失。
先定义本系列反复用到的词:丢失窗口(数据丢失时间窗)------从最后一次"落盘"到故障发生之间的时间。业务能容忍丢失多少秒,直接决定持久化配置:
| 业务 | 丢失容忍 | 合理的持久化策略 |
|---|---|---|
| 商品缓存(可重建) | 分钟级 | RDB 即可,甚至不持久化 |
| 购物车/会话 | 秒级 | AOF everysec |
| 支付流水/状态机 | 零容忍 | AOF always + 主从 + 备份 |
本篇用隔离实例把三种持久化的"保存-恢复-损坏-修复"完整演一遍。
1. RDB:内存快照
1.1 机制:fork + Copy-On-Write
RDB 是某时刻全内存数据的二进制快照 。触发 SAVE(同步)或 BGSAVE(后台)时:
- 主进程
fork()一个子进程; - 子进程把内存中的数据序列化写入
dump.rdb; - 主进程继续服务客户端。
关键在 fork + COW(写时复制):
dump.rdb 子进程(fork) 主进程 dump.rdb 子进程(fork) 主进程 #mermaid-svg-PUgF7t5wZ86Hx5WC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PUgF7t5wZ86Hx5WC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PUgF7t5wZ86Hx5WC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PUgF7t5wZ86Hx5WC .error-icon{fill:#552222;}#mermaid-svg-PUgF7t5wZ86Hx5WC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PUgF7t5wZ86Hx5WC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PUgF7t5wZ86Hx5WC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PUgF7t5wZ86Hx5WC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PUgF7t5wZ86Hx5WC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PUgF7t5wZ86Hx5WC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PUgF7t5wZ86Hx5WC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PUgF7t5wZ86Hx5WC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PUgF7t5wZ86Hx5WC .marker.cross{stroke:#333333;}#mermaid-svg-PUgF7t5wZ86Hx5WC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PUgF7t5wZ86Hx5WC p{margin:0;}#mermaid-svg-PUgF7t5wZ86Hx5WC .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-PUgF7t5wZ86Hx5WC text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-PUgF7t5wZ86Hx5WC .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-PUgF7t5wZ86Hx5WC .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-PUgF7t5wZ86Hx5WC .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-PUgF7t5wZ86Hx5WC .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-PUgF7t5wZ86Hx5WC #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-PUgF7t5wZ86Hx5WC .sequenceNumber{fill:white;}#mermaid-svg-PUgF7t5wZ86Hx5WC #sequencenumber{fill:#333;}#mermaid-svg-PUgF7t5wZ86Hx5WC #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-PUgF7t5wZ86Hx5WC .messageText{fill:#333;stroke:none;}#mermaid-svg-PUgF7t5wZ86Hx5WC .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-PUgF7t5wZ86Hx5WC .labelText,#mermaid-svg-PUgF7t5wZ86Hx5WC .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-PUgF7t5wZ86Hx5WC .loopText,#mermaid-svg-PUgF7t5wZ86Hx5WC .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-PUgF7t5wZ86Hx5WC .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-PUgF7t5wZ86Hx5WC .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-PUgF7t5wZ86Hx5WC .noteText,#mermaid-svg-PUgF7t5wZ86Hx5WC .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-PUgF7t5wZ86Hx5WC .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-PUgF7t5wZ86Hx5WC .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-PUgF7t5wZ86Hx5WC .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-PUgF7t5wZ86Hx5WC .actorPopupMenu{position:absolute;}#mermaid-svg-PUgF7t5wZ86Hx5WC .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-PUgF7t5wZ86Hx5WC .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-PUgF7t5wZ86Hx5WC .actor-man circle,#mermaid-svg-PUgF7t5wZ86Hx5WC line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-PUgF7t5wZ86Hx5WC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} COW:谁先写,谁复制该页 内存页 fork()(页表复制,共享物理页) 继续服务写命令(修改的页被复制) 基于"快照时刻"的数据写文件 完成后通知主进程
COW 的代价:fork 瞬间 复制页表(大实例可能几十~几百毫秒);写命令触发页复制(写入越频繁,复制越多);快照期间内存可能短时上升(被复制的页)。
1.2 触发方式
| 方式 | 命令/配置 | 阻塞性 |
|---|---|---|
| 手动同步 | SAVE |
阻塞主进程直到写完 |
| 手动后台 | BGSAVE |
fork 瞬间阻塞,之后后台写 |
| 自动 | save 900 1 / save 300 10 / save 60 10000 |
同 BGSAVE |
| 关闭自动 | save "" |
手动控制 |
1.3 实测:保存、校验、恢复
在隔离实例上完整走一遍(写入 100 个键 → SAVE → 校验 → 重启):
text
$ redis-cli -p 6380 SAVE
OK
$ ls -la dump.rdb
-rw-r--r-- 1875 dump.rdb ← 二进制快照,1875 字节
$ redis-check-rdb dump.rdb
[offset 1875] Checksum OK
[offset 1875] \o/ RDB looks OK! \o/
[info] 100 keys read
(重启实例)
$ redis-cli -p 6380 DBSIZE
100 ← 数据完整恢复
$ redis-cli -p 6380 GET order:42
data-42
启动日志中的加载证据:
text
* Loading RDB produced by version 8.10.0
* RDB age 15 seconds
* Done loading RDB, keys loaded: 100, keys expired: 0.
RDB 的定位是"定期快照":恢复快(直接载入二进制)、文件紧凑,但丢失窗口 = 距上次快照的时间。
2. AOF:追加式命令日志
2.1 机制:把"写命令"本身记下来
AOF(Append Only File)记录每条修改数据的命令(RESP 协议格式),重启时重放命令重建数据。相比 RDB,它把丢失窗口压缩到"最后一次 fsync 到故障"之间。
写入链路:
text
客户端写命令 → 命令执行(内存生效)
→ 写入 AOF 缓冲
→ 按 appendfsync 策略刷入 OS 文件缓存
→ 按策略 fsync 落盘
appendfsync 三个档位的丢失窗口:
| 策略 | fsync 时机 | 丢失窗口 | 性能 |
|---|---|---|---|
always |
每条命令后 | 0(严格说:命令返回前已落盘) | 最慢,每秒数千~数万次 fsync |
everysec |
每秒一次 | 最多 1 秒 | 生产默认,性能/安全平衡 |
no |
交给 OS 决定 | 取决于 OS 刷盘时机(理论最长可达秒级~分钟级) | 最快 |
2.2 7.0+ 的多部件 AOF:文件结构变了
Redis 7.0 重构了 AOF:不再是单个 appendonly.aof,而是一个 appendonlydir/ 目录,由三部分组成:
text
appendonlydir/
├── appendonly.aof.manifest ← 清单:base/incr 文件与顺序
├── appendonly.aof.1.base.rdb ← base 文件(当前全量;纯 AOF 或混合 RDB 头)
└── appendonly.aof.1.incr.aof ← incr 文件(base 之后的增量命令)
实测的 manifest 内容:
text
file appendonly.aof.1.base.rdb seq 1 type b
file appendonly.aof.1.incr.aof seq 1 type i startoffset 0
incr 文件就是 RESP 协议的命令日志(可读):
text
*1
$5
MULTI
*2
$6
SELECT
$1
0
*3
$3
...
AOF 重写 (BGREWRITEAOF)把当前全量数据写成新的 base 文件,并清空/截断增量------防止 AOF 无限膨胀。重写同样是 fork 子进程 + 后台进行,与 RDB 的 COW 机制一致。
2.3 实测:AOF 保存与恢复
text
$ redis-cli -p 6380 BGREWRITEAOF
Background append only file rewriting started
$ ls appendonlydir/
appendonly.aof.2.base.rdb 21875 字节 ← 重写后的全量
appendonly.aof.2.incr.aof 0 字节
appendonly.aof.manifest
$ redis-check-aof appendonlydir/appendonly.aof.manifest
BASE AOF appendonly.aof.2.base.rdb is valid
All AOF files and manifest are valid
(重启实例)
$ redis-cli -p 6380 DBSIZE
1000 ← 完整恢复
$ redis-cli -p 6380 GET order:99
data-99
3. 混合持久化:RDB 头 + AOF 尾
纯 AOF 在启动加载 时要重放全部命令,数据量大时启动慢。混合持久化(aof-use-rdb-preamble yes,7.x 默认开启)让 AOF 重写后的 base 文件直接用 RDB 格式(快照加载),之后的增量继续用 AOF 追加:
text
$ head -c 9 appendonlydir/appendonly.aof.2.base.rdb | xxd
00000000: 5245 4449 5330 3031 35 REDIS0015 ← RDB 魔数
redis-check-aof 对它的识别:
text
RDB preamble is OK, proceeding with AOF tail...
加载优先级 :只要 AOF 开启,启动时只从 AOF(含混合 base)加载 ,不读 dump.rdb------AOF 永远包含更新的数据。RDB 只在 AOF 关闭时生效。
4. 失败实验:损坏与修复
4.1 AOF 损坏:启动失败 → 修复 → 恢复
向 incr AOF 文件中间插入垃圾字节后启动:
text
# Bad file format reading the append only file appendonlydir/appendonly.aof.2.incr.aof at offset 0.
# make a backup of your AOF file, then use ./redis-check-aof --fix <filename.manifest>.
# Alternatively you can set the 'aof-load-corrupt-tail-max-size' configuration option
# to 28 and restart the server.
注意细节:base 文件加载成功了 (Done loading RDB, keys loaded: 1000),失败在损坏的 incr 段------多部件 AOF 让"全量部分无损,增量部分可修"成为可能。修复:
text
$ redis-check-aof --fix appendonlydir/appendonly.aof.manifest
RDB preamble is OK, proceeding with AOF tail...
...
AOF appendonly.aof.2.incr.aof format error
AOF analyzed: ... ok_up_to=0, diff=28
This will shrink the AOF ... from 28 bytes, with 28 bytes, to 0 bytes
Continue? [y/N]: y
Successfully truncated AOF appendonly.aof.2.incr.aof
All AOF files and manifest are valid
重启后数据完整恢复(1000 键,来自未损坏的 base):
text
$ redis-cli -p 6380 DBSIZE
1000
$ redis-cli -p 6380 GET product:999
data-999
修复原则 :先备份损坏文件,再 --fix;--fix 会截断损坏点之后的所有内容(丢失"损坏点之后"的数据,保留之前)。
4.2 RDB 损坏:CRC 校验拦截
破坏 dump.rdb 的 checksum 后:
text
$ redis-check-rdb dump.rdb
--- RDB ERROR DETECTED ---
[offset 7375] RDB CRC error
[additional info] While doing: check-sum
[info] 500 keys read
启动时同样被拦截:
text
# Wrong RDB checksum expected: (5baeea71929c254a) but got (a451158e6d63dab5). Aborting now.
# Internal error in RDB reading offset 0, function at rdb.c:5062 -> RDB CRC error
Redis 拒绝加载损坏的 RDB 并直接退出 (默认 rdbchecksum yes 且不做自动修复)------这比"带病启动"安全:宁可起不来,也不给应用喂一份被篡改的数据。生产修复路径是:用备份 RDB → 或用 redis-check-rdb 报告损坏程度 → 必要时结合 AOF 恢复。
4.3 "断电"实验:appendfsync no 的意外结果
用 taskkill /F 强制杀掉 AOF(appendfsync no)实例模拟断电,重启后 1000 个键全部还在:
text
$ taskkill /F /PID 46440 # 模拟断电
成功: 已终止 PID 为 46440 的进程。
(重启)
$ redis-cli -p 6380 DBSIZE
1000
这个结果必须谨慎解读 :appendfsync no 只是"不主动 fsync",数据仍会经 OS 文件缓存 异步刷盘------Windows 上缓存刷盘非常快,所以"写入后立即 kill"通常已经落盘。它不能证明 no 策略安全 :如果 OS 自身崩溃或整机断电,尚未刷盘的缓存照样丢;丢失窗口在理论上依然存在且不可控。这个实验的意义是说明:"实测没丢"≠"策略安全",丢失窗口是概率与环境的函数 。生产仍应选 everysec(丢失窗口有界 ≤ 1s)或 always(零丢失)。
5. 持久化策略设计
5.1 三种方式对比
| 维度 | RDB | AOF(everysec) | 混合(默认) |
|---|---|---|---|
| 丢失窗口 | 距上次快照(分钟级) | ≤ 1 秒 | ≤ 1 秒 |
| 恢复速度 | 快(二进制载入) | 慢(重放命令) | 快(RDB 头 + 增量) |
| 文件大小 | 紧凑 | 可能膨胀(需重写) | 紧凑(重写后 RDB 头) |
| 写路径开销 | fork + COW | 追加 + 每秒 fsync | 同 AOF |
| 故障文件修复 | redis-check-rdb(只报告) |
redis-check-aof --fix(可截断修复) |
同 AOF |
5.2 生产推荐组合
text
appendonly yes # 打开 AOF
appendfsync everysec # 丢失窗口 ≤ 1s(默认)
aof-use-rdb-preamble yes # 混合持久化(默认)
save 900 1 300 10 60 10000 # 定期 RDB 作为额外备份点
再加三道保险(后续 Part 10/12 展开):
- 主从复制:至少一个从库,主库故障自动切换;
- 异地备份:定期把 RDB/快照拷贝到其他存储;
- 恢复演练:周期性地"备份 → 恢复到干净实例 → 校验数据"。
6. 版本与环境差异
| 差异点 | 7.0 之前 | 7.x / 本机 8.10 | 影响 |
|---|---|---|---|
| AOF 文件形态 | 单个 appendonly.aof |
多部件目录 appendonlydir/(manifest + base + incr) |
备份/修复命令参数变化;redis-check-aof --fix 传 manifest 文件 |
aof-use-rdb-preamble |
4.0 引入,5.0 起默认开启 | 默认 yes(混合持久化) | base 文件是 .rdb 格式 |
| AOF 加载失败策略 | 直接拒绝 | 仍拒绝,但可配 aof-load-corrupt-tail-max-size 容忍尾部损坏 |
高可用场景可有限容忍 |
rdbchecksum |
默认 yes | 默认 yes(CONFIG GET 实测) |
损坏 RDB 启动即 abort(§4.2) |
注意 :损坏与断电实验均在隔离实例 6380 上进行,未触碰生产主实例 6379------这类实验永远不要在生产/开发共享实例上做。
7. 测试与验收
- 建议测试:备份/恢复往返(SAVE → 重启 → 校验键数)、
redis-check-*命令可用性、损坏文件修复脚本(隔离实例); - 生产验收清单见下。
本篇验收清单:
- 能画出 fork + COW 的时序并说出 RDB 的三个阻塞点(fork、页复制、写盘);
- 能说出
appendfsync三档的丢失窗口(0 / ≤1s / 取决于 OS); - 知道 7.0+ AOF 是多部件目录结构,base/incr/manifest 各自职责;
- 知道混合持久化的加载流程(RDB 头 + AOF 尾)与"AOF 优先于 RDB"的规则;
- 能完成一次 AOF 损坏修复(备份 →
--fix→ 重启验证); - 能为"秒级容忍/零容忍/可重建"三类业务各设计一套持久化配置。
8. 常见误区
- "Redis 默认持久化,重启不丢数据"------默认只 RDB 快照且触发频率低,丢失窗口分钟级起步(§0)。
- "
SAVE和BGSAVE一样安全" ------SAVE同步阻塞主进程;生产手动触发永远用BGSAVE。 - "AOF 是日志文件,改一行加一行"------7.0+ 是多部件结构 + 重写机制;直接编辑文件是灾难(§2.2)。
- "RDB 损坏了
redis-check-rdb --fix能修" ------redis-check-rdb只报告不修复;修复靠备份或 AOF(§4.2)。 - "实测 kill -9 没丢数据,所以 appendfsync no 安全"------丢失窗口是概率性的,OS 崩溃/断电才是 no 的真正风险(§4.3)。
- "开启 AOF 后 RDB 就没用了"------RDB 仍是恢复快、文件小的备份点;混合持久化的 base 就是 RDB 格式(§5)。
9. 本篇小结
回到开篇:持久化的本质是用可控的写路径开销,换取有界的丢失窗口:
- RDB:全量快照,恢复最快,丢失窗口 = 距上次快照;
- AOF:命令追加 + fsync 策略,丢失窗口有界(everysec ≤ 1s);
- 混合:base 用 RDB(快加载)+ incr 用 AOF(低丢失),7.x 默认形态;
- 损坏防御:RDB 靠 CRC 校验拒绝启动,AOF 靠
redis-check-aof --fix截断修复------备份永远是最可靠的恢复手段。
下一篇 Part 6:过期、内存淘汰与内存优化 把"内存"管起来:TTL 的惰性/定期删除、maxmemory 八种淘汰策略、大 key 识别与内存治理,回答"设置了过期时间为什么内存还在涨"。
10. 官方资料
- Redis persistence 官方文档:https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/
- RDB 文件格式说明:https://rdb.readthedocs.io/
redis-check-aof/redis-check-rdb:https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/BGREWRITEAOF/BGSAVE:https://redis.io/docs/latest/commands/