Redis 系列(五):持久化——RDB、AOF 与混合持久化

核心目标:理解 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(后台)时:

  1. 主进程 fork() 一个子进程;
  2. 子进程把内存中的数据序列化写入 dump.rdb
  3. 主进程继续服务客户端。

关键在 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 yes7.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 展开):

  1. 主从复制:至少一个从库,主库故障自动切换;
  2. 异地备份:定期把 RDB/快照拷贝到其他存储;
  3. 恢复演练:周期性地"备份 → 恢复到干净实例 → 校验数据"。

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. 常见误区

  1. "Redis 默认持久化,重启不丢数据"------默认只 RDB 快照且触发频率低,丢失窗口分钟级起步(§0)。
  2. "SAVEBGSAVE 一样安全" ------SAVE 同步阻塞主进程;生产手动触发永远用 BGSAVE
  3. "AOF 是日志文件,改一行加一行"------7.0+ 是多部件结构 + 重写机制;直接编辑文件是灾难(§2.2)。
  4. "RDB 损坏了 redis-check-rdb --fix 能修" ------redis-check-rdb 只报告不修复;修复靠备份或 AOF(§4.2)。
  5. "实测 kill -9 没丢数据,所以 appendfsync no 安全"------丢失窗口是概率性的,OS 崩溃/断电才是 no 的真正风险(§4.3)。
  6. "开启 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. 官方资料

相关推荐
龙仔7251 小时前
人大金仓Kingbase V8 自动全量备份部署完整笔记
数据库·笔记·备份·人大金仓
王志来137944730081 小时前
从选型到交付,工控服务器机箱一站式采购平台匀天如何实现服务闭环?
数据库
ShuiShenHuoLe2 小时前
HmarkX(码笺)的本地数据库模块重构实战
数据库·oracle·重构
Databend2 小时前
从 Kafka 到 Databend Cloud:万亿级 Agent Trace 接入链路的工程实践
大数据·数据库·agent
倔强的石头_2 小时前
SQL Server数据迁移,不只是把数据搬过去
数据库
acd120092 小时前
Redis 缓存和数据库一致性,到底怎么搞?
数据库·redis·缓存
sugar__salt2 小时前
MyBatis-Plus 从入门到实战:高效简化数据库开发的完整指南
数据库·spring boot·mybatis·数据库开发
CodeBlog-star2 小时前
AI Agent 高并发实战:Redis 限流、队列、缓存与高可用全栈方案
数据库·redis·缓存
TDengine (老段)2 小时前
TDengine taosX 与 Explorer — 数据集成与可视化管理
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据