
上一篇我们沿着一条 HGET 拆开了 Redis 的执行链路:请求进入事件循环,主线程访问内存中的数据结构,再把响应写回客户端。内存让 Redis 足够快,也留下了一个绕不过去的问题------进程一旦退出,内存里的秒杀库存、用户会话和订单事件还剩多少?
"Redis 有 RDB 和 AOF,所以数据不会丢"是一个危险的简化。RDB 保存的是某个时间点的快照,两次快照之间的修改可能消失;AOF 记录写命令,但命令写进用户态缓冲、内核页缓存和真正落到持久化介质,是三个不同阶段。即使客户端已经收到 OK,也不代表这次修改一定越过了所有故障边界。
这篇从"Redis 到底要防哪种故障"开始,拆解 RDB 的 fork 与 Copy-on-Write、AOF 的写入和 fsync、AOF 重写与 Redis 7+ 多文件格式、混合持久化和启动恢复,最后把性能抖动、容量规划与恢复演练放回生产环境。
目录
- 持久化到底在防什么
- RDB:给内存数据拍一张快照
- AOF:把每次写操作追加成日志
- [AOF 重写:日志为什么不会无限增长](#AOF 重写:日志为什么不会无限增长)
- 混合持久化与启动恢复
- [RDB、AOF 应该怎么选](#RDB、AOF 应该怎么选)
- 持久化为什么会反过来影响延迟
- 生产配置与故障恢复清单
一、持久化到底在防什么
先不要急着背 RDB 和 AOF。讨论"会不会丢数据"之前,必须先说明发生了什么故障:
| 故障 | 典型现象 | 持久化能解决到什么程度 |
|---|---|---|
| Redis 进程崩溃 | 操作系统和磁盘仍正常 | 已经落盘的 RDB/AOF 可以恢复 |
| 操作系统崩溃或机器断电 | 内核页缓存也可能丢失 | 取决于最近一次 fsync 是否完成 |
| 磁盘损坏或整机丢失 | 本机持久化文件不可读 | 单机持久化不够,需要副本和异地备份 |
| 人为误删、错误命令 | 错误也会被正常持久化和复制 | 需要备份、审计和可恢复时间点 |
持久化解决的是"内存状态怎样写到非易失介质",不是完整的容灾系统。复制也不能替代持久化:主节点误执行 FLUSHALL,命令可能立即复制到从节点;主从都不落盘,同一机房掉电后仍可能一起失去数据。

1.1 客户端收到 OK,不等于数据已经落盘
一条写命令可能依次经过这些状态:
text
命令在主线程执行完成
↓
修改已经存在于 Redis 内存
↓
AOF 内容进入 Redis 用户态缓冲
↓
write 写入操作系统页缓存
↓
fsync 请求把数据刷向持久化介质
↓
存储设备确认完成
客户端何时收到响应,与 AOF 采用什么刷盘策略有关。appendfsync always 追求每次写后刷盘,everysec 通常每秒刷一次,no 则主要交给操作系统决定。三种策略对应不同的性能、尾延迟和数据丢失窗口。
所以更准确的问题不是"Redis 会不会丢数据",而是:在指定故障模型下,业务最多允许丢多久的数据,恢复要花多久,愿意为此付出多少写入延迟与资源成本。
1.2 RPO 和 RTO 是两把尺子
- RPO(Recovery Point Objective,恢复点目标) :故障后最多允许回退多久。每小时一份 RDB,理论丢失窗口就可能接近一小时;AOF
everysec通常把窗口压到约一秒量级。 - RTO(Recovery Time Objective,恢复时间目标):从故障发生到服务恢复要多久。文件越大、回放命令越多,加载通常越慢;但能否快速拉起还取决于磁盘、CPU、数据量和部署平台。
RPO 更小不代表方案一定更好。如果 Redis 只是可回源的商品详情缓存,允许重建,持久化甚至可以关闭;如果 Redis 承担暂未进入数据库的订单状态,丢一秒也可能不可接受,此时就不应只依赖单实例 Redis。
二、RDB:给内存数据拍一张快照
RDB 把某一时刻的数据库状态序列化为紧凑的二进制文件。它保存"现在有哪些 Key、Value、过期时间和必要元数据",而不是保存从启动以来执行过的每一条命令。
触发方式主要有三类:
redis
SAVE
BGSAVE
CONFIG GET save
SAVE在主线程同步生成快照,期间无法继续正常处理客户端请求,生产环境通常不应手工使用;BGSAVEfork 子进程生成 RDB,父进程继续处理命令,是常用方式;save规则可以在"指定时间内至少发生多少次修改"后自动触发后台快照,例如默认配置语义中的3600 1 300 100 60 10000。
2.1 BGSAVE 怎样边写业务边生成快照
BGSAVE 开始时,主进程调用 fork 创建子进程。子进程继承父进程的虚拟地址空间视图,随后遍历数据并写出临时 RDB;写完、校验成功后再用新文件替换旧快照。

父子进程起初共享同一批物理内存页。快照期间如果父进程修改某一页,操作系统通过 Copy-on-Write(写时复制,COW) 为修改方复制页面:
text
fork 时:父进程 ─┐
├── 共享旧内存页
子进程 ──┘
发生写入后:父进程 → 新页(继续处理最新数据)
子进程 → 旧页(保持快照时刻视图)
因此子进程看到的是 fork 时刻附近的一致数据视图,父进程仍能对外服务。但"后台保存"不等于没有成本:
fork本身要复制页表,数据集越大、内存页越多,暂停越明显;- 快照期间写入越多,COW 复制的内存页越多,RSS 可能快速上涨;
- 子进程顺序扫描内存并写磁盘,会竞争 CPU、内存带宽和磁盘 I/O;
- 内存不足时,COW 放大可能触发 Swap 或 OOM,造成比快照失败更严重的抖动。
2.2 RDB 会丢多少数据
假设 12:00 生成了一份 RDB,下一次计划在 12:05 生成。机器在 12:04:59 断电,最近四分多钟的修改没有进入已完成的快照,重启只能回到 12:00。
RDB 的丢失窗口取决于快照间隔和最后一次快照是否成功,而不是文件格式本身。手工每天备份一份 RDB,适合灾难恢复,却不适合承诺秒级 RPO。
2.3 RDB 的优势与边界
RDB 的优势很明确:文件紧凑,便于复制和归档;恢复时直接反序列化最终状态,通常比逐条回放大量命令更快;生成工作主要由子进程完成。
它的边界也同样明确:快照之间存在数据窗口;fork、COW 和磁盘写入会带来资源峰值;快照只是某个时间点,不能单独表达这之后发生的修改。AOF 就是为"记录增量写入"而存在的。
三、AOF:把每次写操作追加成日志
AOF(Append Only File)不定期拍整库照片,而是把会改变数据的命令按 Redis 协议追加记录。恢复时重新加载基础数据,再按顺序应用增量命令,就能重建故障前的状态。
conf
appendonly yes
appendfsync everysec
3.1 一条写命令怎样进入 AOF
客户端执行:
redis
SET product:stock:1001 499
EXPIRE product:stock:1001 600
命令执行成功后,Redis 把需要持久化的写操作编码到 AOF 缓冲。事件循环在合适阶段调用 write,数据先进入操作系统页缓存;是否等待 fsync,由刷盘策略决定。

这里要区分三个动作:
- append 到 AOF 缓冲:Redis 进程内存中的待写数据;
- write:把字节交给内核页缓存,进程崩溃后通常仍在,但机器断电可能丢;
- fsync:请求操作系统把相关文件数据同步到持久化介质,是跨越掉电边界的关键动作。
"写了 AOF"如果没有说明到了哪一层,结论仍然不完整。
3.2 三种 appendfsync 策略
| 策略 | 刷盘方式 | 典型数据窗口 | 代价与适用倾向 |
|---|---|---|---|
always |
每次写入后执行 fsync |
最小,通常只冒险丢正在执行的一条命令 | 写延迟直接受磁盘影响,吞吐最低 |
everysec |
后台线程通常每秒 fsync |
正常情况下约一秒量级 | 性能与安全的常用折中,也是默认建议 |
no |
Redis 不主动按每次/每秒刷盘 | 由操作系统刷新节奏决定,窗口更大 | 吞吐更高,但故障边界更弱 |
everysec 不是"绝对只丢一秒"。存储设备、文件系统、虚拟化层和故障方式都会影响最终结果;它表达的是正常工作机制下的目标窗口,而不是跨所有硬件故障的数学保证。
另一方面,always 也不能把单机变成绝对可靠存储。磁盘控制器谎报完成、文件系统损坏、整盘丢失和人为误删,都超出单次 fsync 能解决的范围。更强的数据安全仍需要副本、备份、跨故障域部署和业务侧幂等。
3.3 AOF 为什么比原始客户端命令更适合恢复
AOF 记录的是 Redis 认可并执行后的写入语义,不是简单抓取网络包。某些命令会被转成更适合重放的形式,例如带相对过期时间的操作需要避免恢复时重新获得完整 TTL。
恢复的目标不是逐字复刻客户端输入,而是让最终数据状态一致。也正因为它按命令累积,同一个 Key 被改写一万次,旧日志仍会留在文件中;这就需要 AOF 重写压缩历史。
四、AOF 重写:日志为什么不会无限增长
假设秒杀库存从 10000 连续递减到 100,AOF 可能积累 9900 次修改。但恢复最终状态并不需要保留全部历史,只需一条能构造当前值的命令:
redis
SET product:stock:1001 100
AOF 重写不是读取旧 AOF、合并相邻命令,而是遍历当前内存数据集,为每个对象生成足以恢复当前状态的最小表示。这样可以删除已经被覆盖、过期或删除的历史操作。
4.1 Redis 7+ 的多文件 AOF
Redis 7 开始使用多文件 AOF,由三类文件共同组成:
- BASE 文件:某次重写生成的完整基础状态,可以是 RDB 二进制格式,也可以是 AOF 命令格式;
- INCR 文件:BASE 之后发生的增量写命令,可以存在多个;
- manifest 文件:记录有效文件及加载顺序,相当于这组 AOF 的目录。

一次后台重写可以概括为:
- fork 后台子进程,根据当前内存视图生成新的 BASE;
- 主进程继续处理请求,并把新写命令追加到新的 INCR 文件;
- BASE 生成成功后,Redis 原子更新 manifest,让"新 BASE + 新 INCR"成为有效集合;
- 不再被新 manifest 引用的旧文件随后清理。
这种设计避免让一个巨型 AOF 文件同时承担基础快照、在线追加和原子替换,也让重写期间的增量边界更清楚。
4.2 自动重写阈值怎样理解
默认配置中常见:
conf
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
含义不是"AOF 每到 64 MB 就重写"。Redis 会比较当前 AOF 体积和上次重写后的基准体积:只有当前文件至少达到最小值,并且相对基准增长达到指定百分比,才触发自动重写。
阈值太小会频繁 fork 和写盘;阈值太大则让磁盘占用、恢复时间和下一次重写工作量持续上升。生产值应结合写入速率、磁盘空间、允许的重写频率和恢复目标压测,而不是机械复制默认值。
4.3 重写期间要不要暂停 fsync
conf
no-appendfsync-on-rewrite no
保持 no 更偏向数据安全:即使后台正在生成 RDB 或重写 AOF,主进程仍按策略推进 fsync。某些 Linux 与磁盘环境下,后台大写入可能让同步调用出现长延迟。
改成 yes 可以缓解部分抖动,但重写期间会暂时接近 appendfsync no 的耐久性,最坏数据窗口可能明显扩大。它不是免费的性能开关,必须和业务 RPO 一起决策。
五、混合持久化与启动恢复
RDB 恢复快但增量窗口大,纯命令 AOF 更完整却可能体积大、加载慢。Redis 的常用组合是:AOF 的 BASE 使用 RDB 格式,后续变化继续写 INCR AOF。
conf
aof-use-rdb-preamble yes
Redis 7+ 的 BASE 文件名可能类似:
text
appendonly.aof.1.base.rdb
appendonly.aof.1.incr.aof
appendonly.aof.manifest
启动时先快速加载 RDB 格式的 BASE,再顺序回放 INCR 中的写命令。它兼顾 RDB 的紧凑恢复与 AOF 的较小数据窗口,这就是常说的混合持久化。
5.1 同时存在 RDB 和 AOF 时加载谁
如果 AOF 已启用,Redis 启动时优先用 AOF 文件集合恢复,因为它通常包含比独立 RDB 更新的数据;只有没有启用 AOF 时,才依靠 RDB 快照恢复。
不要以为"两个都开"就会把两个文件合并加载。它们承担的是不同角色:AOF 是启动恢复的主路径,独立 RDB 仍可用于周期备份、跨环境传输和灾难恢复。
5.2 AOF 尾部截断和中间损坏不是一回事
机器异常掉电时,AOF 最后一个命令可能只写了一半。默认配置:
conf
aof-load-truncated yes
Redis 可以忽略不完整的尾部,加载前面有效内容并记录警告。但如果文件中间发生格式损坏,Redis 通常会拒绝启动,因为继续跳过可能让后续命令在错误状态上执行。
修复前应先复制原始文件,再使用对应版本的 redis-check-aof 检查;不要直接对唯一副本做截断。修复工具的目标是找回"最后一个可安全重放的位置",被截掉的尾部数据仍然需要业务补偿或从其他副本恢复。
5.3 备份文件不等于备份完成
把 dump.rdb 或 appendonlydir 留在同一块磁盘,只能应对进程级故障,不能应对磁盘损坏、宿主机丢失和机房故障。合格的备份链路至少要包含:
- 复制到不同故障域,而不是同目录改个文件名;
- 保留多个时间点,避免错误操作覆盖所有副本;
- 校验文件可读性与校验和;
- 定期在隔离环境真实启动并验证关键 Key,而不是只看"上传成功"。
六、RDB、AOF 应该怎么选
没有脱离业务角色的标准答案。可以先看这张对比表:
| 维度 | RDB | AOF | RDB BASE + AOF INCR |
|---|---|---|---|
| 记录内容 | 某时刻最终状态 | 增量写命令 | 基础状态 + 后续增量 |
| 常见 RPO | 取决于快照间隔,通常更大 | everysec 约秒级目标 |
通常与 AOF 策略一致 |
| 文件体积 | 紧凑 | 纯命令历史较大 | 通常更紧凑 |
| 恢复方式 | 直接加载状态 | 回放命令 | 加载 BASE 后回放 INCR |
| 主要资源峰值 | fork、COW、全量写盘 | 持续 write/fsync、重写 | 同时具备两者特征 |
| 适合角色 | 可重建缓存、周期备份 | 更重视数据完整性 | 常见生产折中 |
6.1 三类典型业务选择
可完全回源的商品缓存:数据源在 MySQL,Redis 只是加速层。可以只用 RDB 做冷启动加速,甚至关闭持久化,让节点故障后从数据库和副本重建。关键是防止大量 Key 同时回源压垮数据库。
允许秒级窗口的会话和排行榜 :可以启用 AOF everysec,同时保留周期 RDB 备份。业务要接受极端故障下最近少量变化可能丢失,并设计会话重登或排行榜校正。
订单、余额等不可丢核心事实 :不应因为 appendfsync always 就把单机 Redis 当最终账本。Redis 可以承担加速、并发控制或临时状态,但最终事实应进入具备明确事务、复制和备份保证的持久化系统;业务还要使用唯一请求号、幂等和对账补偿。
6.2 "RDB + AOF"不是风险简单相加为零
同时开启能提供更灵活的恢复材料,却不能消除共同故障:两者都依赖同一台机器的内存、CPU 和磁盘;一次磁盘损坏可能同时破坏 RDB 与 AOF;fork 和重写也可能在相近时间争抢资源。
高可用设计要把持久化、副本、备份和故障域放在一起看:
text
持久化:单节点重启后能恢复到哪里
复制:节点故障时谁可以接管
备份:误操作或整组损坏后回到哪个历史点
业务补偿:无法自动恢复的最后缺口怎样对账
七、持久化为什么会反过来影响延迟
Redis 命令主要操作内存,但持久化会把 fork、页表复制、COW、磁盘吞吐和 fsync 延迟带回主链路。线上出现周期性 P99 尖刺时,不能只盯 Slow Log。
7.1 fork 延迟
fork 不会复制全部数据页,却需要创建子进程并复制页表。实例越大、内存映射越复杂,暂停通常越明显。透明大页、宿主机 CPU 争抢和内存压力还会进一步放大停顿。
可以结合这些指标判断:
redis
INFO persistence
INFO stats
LATENCY DOCTOR
重点关注 latest_fork_usec、后台保存/重写状态、最近一次成功时间与失败状态。不要只看到 BGSAVE 的"BG"就认定主线程完全无感。
7.2 COW 内存放大
快照或重写期间,写流量越密集,被修改的共享页越多。一个占用 20 GB 的实例,不代表留 1 GB 空闲就足以安全 BGSAVE。最坏情况与写入模式、内存碎片和页面分布有关,必须给 COW 和操作系统留出余量。
如果宿主机开始 Swap,Redis 的微秒级内存访问会退化成磁盘访问,延迟可能瞬间失控。生产环境应禁用或严格控制 Swap,并监控 used_memory_rss、mem_fragmentation_ratio、COW 大小和系统可用内存。
7.3 磁盘竞争和 fsync 尾延迟
子进程写 RDB/BASE、主进程追加 INCR、后台线程 fsync,可能同时争用一块盘。云盘突发额度耗尽、宿主机邻居噪声和文件系统回写,都可能让一次原本很快的 write 或 fsync 变慢。
优化时优先做这些事:
- 把数据目录放在延迟稳定、吞吐有余量的磁盘,而不是只看标称 IOPS;
- 避免 Redis 持久化与数据库、日志系统共享同一块繁忙磁盘;
- 监控磁盘队列、写延迟、吞吐和空间,关联
BGSAVE/重写时间点看 P99; - 控制单实例规模,让 fork、恢复和重写都在可接受时间内完成;
- 在真实数据量和写入比例下压测,不用空实例的结果推导生产。
7.4 磁盘写满比"变慢"更危险
AOF 持续增长、重写又需要额外空间。只按最终 BASE 大小预留磁盘,可能在重写同时存在旧文件、新 BASE 和新 INCR 时耗尽空间。
RDB 保存失败时,默认 stop-writes-on-bgsave-error yes 会在启用快照规则的情况下停止接收写请求,避免应用在持久化已经失效时继续无感写入。关闭这项保护前,必须有可靠告警和明确的降级策略。
八、生产配置与故障恢复清单
8.1 上线前先写清四个答案
- Redis 中的数据能否从权威数据源重建?
- 进程崩溃、机器掉电、整盘损坏时分别允许丢多久?
- 10 GB、50 GB 数据实际加载需要多久,是否满足 RTO?
- 谁负责发现持久化失败、执行切换、恢复文件和业务补偿?
如果这四个问题没有答案,appendonly yes 只是一项配置,不是一套可靠性方案。
8.2 一份偏通用的检查起点
conf
# 周期快照:按业务调整,不要把示例当最终答案
save 3600 1 300 100 60 10000
stop-writes-on-bgsave-error yes
# 更重视秒级 RPO 时启用 AOF
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes
# 重写阈值需要结合数据规模和磁盘空间压测
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
no-appendfsync-on-rewrite no
这组配置表达的是"安全优先的常见起点",不是所有业务的最优解。纯缓存可以更轻,核心数据则需要更强的外部持久化、复制和备份。
版本提示 :Redis 7+ 使用 BASE、INCR 与 manifest 组成多文件 AOF,默认
aof-use-rdb-preamble yes,BASE 通常采用 RDB 格式。本文以 Redis 7.4 与 Redis 8.0 共享机制为基线;从旧版本升级或迁移 AOF 时,应按目标版本官方升级说明操作,不要直接套用旧版"单个 appendonly.aof 文件"的运维脚本。
8.3 恢复演练比配置截图更可信
至少定期完成一次完整演练:
- 在隔离环境复制 RDB 或整个
appendonlydir; - 校验文件权限、manifest 引用和磁盘空间;
- 使用相同大版本启动 Redis,记录实际加载耗时;
- 抽查库存、会话、排行榜等关键 Key 的数量、TTL 和内容;
- 模拟 AOF 尾部截断,验证告警与修复流程;
- 记录缺失数据怎样从数据库、消息日志或业务流水补齐。
只有真正恢复过的备份,才算具备可验证的恢复能力。
8.4 一条完整的决策链
最终可以用下面这条链路判断:
text
先定义数据角色与 RPO/RTO
↓
选择 RDB、AOF 或混合持久化
↓
为 fork、COW、重写和磁盘预留资源
↓
用副本跨节点,用备份跨时间与故障域
↓
定期恢复演练,用业务对账填补最后缺口
这一篇把"Redis 数据不丢"拆成了可验证的故障边界:RDB 保存某一时刻的最终状态,适合紧凑备份和快速加载,但两次快照之间存在窗口;AOF 追加增量写命令,通过 appendfsync 在性能和耐久性之间取舍;Redis 7+ 又用 BASE、INCR 和 manifest 把重写与在线追加拆开,并默认让 RDB 格式 BASE 配合增量 AOF 恢复。
带走四句话:① 客户端收到 OK 不等于数据已经越过掉电边界,write 与 fsync 必须分开理解;② BGSAVE 不阻塞式生成文件,却仍有 fork、COW、内存和磁盘竞争;③ AOF 重写基于当前内存状态生成最小表示,不是逐行压缩旧日志;④ 持久化只能解决单节点恢复的一部分问题,副本、异地备份、恢复演练和业务补偿缺一不可。
下一篇,我们继续追问"数据留在内存里时,Redis 怎样决定它什么时候消失"------《Redis 06 · 内存管理:过期删除、淘汰策略与大 Key 治理》。TTL 到期为什么不会立刻删除?内存满了以后 LRU、LFU 和随机淘汰怎么选?BigKey、HotKey 和内存碎片又怎样把一个看似健康的实例拖慢?