Redis 05 · 持久化:RDB 与 AOF 怎么保证数据不丢

上一篇我们沿着一条 HGET 拆开了 Redis 的执行链路:请求进入事件循环,主线程访问内存中的数据结构,再把响应写回客户端。内存让 Redis 足够快,也留下了一个绕不过去的问题------进程一旦退出,内存里的秒杀库存、用户会话和订单事件还剩多少?

"Redis 有 RDB 和 AOF,所以数据不会丢"是一个危险的简化。RDB 保存的是某个时间点的快照,两次快照之间的修改可能消失;AOF 记录写命令,但命令写进用户态缓冲、内核页缓存和真正落到持久化介质,是三个不同阶段。即使客户端已经收到 OK,也不代表这次修改一定越过了所有故障边界。

这篇从"Redis 到底要防哪种故障"开始,拆解 RDB 的 fork 与 Copy-on-Write、AOF 的写入和 fsync、AOF 重写与 Redis 7+ 多文件格式、混合持久化和启动恢复,最后把性能抖动、容量规划与恢复演练放回生产环境。

目录

  1. 持久化到底在防什么
  2. RDB:给内存数据拍一张快照
  3. AOF:把每次写操作追加成日志
  4. [AOF 重写:日志为什么不会无限增长](#AOF 重写:日志为什么不会无限增长)
  5. 混合持久化与启动恢复
  6. [RDB、AOF 应该怎么选](#RDB、AOF 应该怎么选)
  7. 持久化为什么会反过来影响延迟
  8. 生产配置与故障恢复清单

一、持久化到底在防什么

先不要急着背 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 在主线程同步生成快照,期间无法继续正常处理客户端请求,生产环境通常不应手工使用;
  • BGSAVE fork 子进程生成 RDB,父进程继续处理命令,是常用方式;
  • save 规则可以在"指定时间内至少发生多少次修改"后自动触发后台快照,例如默认配置语义中的 3600 1 300 100 60 10000

2.1 BGSAVE 怎样边写业务边生成快照

BGSAVE 开始时,主进程调用 fork 创建子进程。子进程继承父进程的虚拟地址空间视图,随后遍历数据并写出临时 RDB;写完、校验成功后再用新文件替换旧快照。

父子进程起初共享同一批物理内存页。快照期间如果父进程修改某一页,操作系统通过 Copy-on-Write(写时复制,COW) 为修改方复制页面:

text 复制代码
fork 时:父进程 ─┐
                 ├── 共享旧内存页
        子进程 ──┘

发生写入后:父进程 → 新页(继续处理最新数据)
            子进程 → 旧页(保持快照时刻视图)

因此子进程看到的是 fork 时刻附近的一致数据视图,父进程仍能对外服务。但"后台保存"不等于没有成本:

  1. fork 本身要复制页表,数据集越大、内存页越多,暂停越明显;
  2. 快照期间写入越多,COW 复制的内存页越多,RSS 可能快速上涨;
  3. 子进程顺序扫描内存并写磁盘,会竞争 CPU、内存带宽和磁盘 I/O;
  4. 内存不足时,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 的目录。

一次后台重写可以概括为:

  1. fork 后台子进程,根据当前内存视图生成新的 BASE;
  2. 主进程继续处理请求,并把新写命令追加到新的 INCR 文件;
  3. BASE 生成成功后,Redis 原子更新 manifest,让"新 BASE + 新 INCR"成为有效集合;
  4. 不再被新 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.rdbappendonlydir 留在同一块磁盘,只能应对进程级故障,不能应对磁盘损坏、宿主机丢失和机房故障。合格的备份链路至少要包含:

  1. 复制到不同故障域,而不是同目录改个文件名;
  2. 保留多个时间点,避免错误操作覆盖所有副本;
  3. 校验文件可读性与校验和;
  4. 定期在隔离环境真实启动并验证关键 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_rssmem_fragmentation_ratio、COW 大小和系统可用内存。

7.3 磁盘竞争和 fsync 尾延迟

子进程写 RDB/BASE、主进程追加 INCR、后台线程 fsync,可能同时争用一块盘。云盘突发额度耗尽、宿主机邻居噪声和文件系统回写,都可能让一次原本很快的 writefsync 变慢。

优化时优先做这些事:

  • 把数据目录放在延迟稳定、吞吐有余量的磁盘,而不是只看标称 IOPS;
  • 避免 Redis 持久化与数据库、日志系统共享同一块繁忙磁盘;
  • 监控磁盘队列、写延迟、吞吐和空间,关联 BGSAVE/重写时间点看 P99;
  • 控制单实例规模,让 fork、恢复和重写都在可接受时间内完成;
  • 在真实数据量和写入比例下压测,不用空实例的结果推导生产。

7.4 磁盘写满比"变慢"更危险

AOF 持续增长、重写又需要额外空间。只按最终 BASE 大小预留磁盘,可能在重写同时存在旧文件、新 BASE 和新 INCR 时耗尽空间。

RDB 保存失败时,默认 stop-writes-on-bgsave-error yes 会在启用快照规则的情况下停止接收写请求,避免应用在持久化已经失效时继续无感写入。关闭这项保护前,必须有可靠告警和明确的降级策略。

八、生产配置与故障恢复清单

8.1 上线前先写清四个答案

  1. Redis 中的数据能否从权威数据源重建?
  2. 进程崩溃、机器掉电、整盘损坏时分别允许丢多久?
  3. 10 GB、50 GB 数据实际加载需要多久,是否满足 RTO?
  4. 谁负责发现持久化失败、执行切换、恢复文件和业务补偿?

如果这四个问题没有答案,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 恢复演练比配置截图更可信

至少定期完成一次完整演练:

  1. 在隔离环境复制 RDB 或整个 appendonlydir
  2. 校验文件权限、manifest 引用和磁盘空间;
  3. 使用相同大版本启动 Redis,记录实际加载耗时;
  4. 抽查库存、会话、排行榜等关键 Key 的数量、TTL 和内容;
  5. 模拟 AOF 尾部截断,验证告警与修复流程;
  6. 记录缺失数据怎样从数据库、消息日志或业务流水补齐。

只有真正恢复过的备份,才算具备可验证的恢复能力。

8.4 一条完整的决策链

最终可以用下面这条链路判断:

text 复制代码
先定义数据角色与 RPO/RTO
    ↓
选择 RDB、AOF 或混合持久化
    ↓
为 fork、COW、重写和磁盘预留资源
    ↓
用副本跨节点,用备份跨时间与故障域
    ↓
定期恢复演练,用业务对账填补最后缺口

这一篇把"Redis 数据不丢"拆成了可验证的故障边界:RDB 保存某一时刻的最终状态,适合紧凑备份和快速加载,但两次快照之间存在窗口;AOF 追加增量写命令,通过 appendfsync 在性能和耐久性之间取舍;Redis 7+ 又用 BASE、INCR 和 manifest 把重写与在线追加拆开,并默认让 RDB 格式 BASE 配合增量 AOF 恢复。

带走四句话:① 客户端收到 OK 不等于数据已经越过掉电边界,writefsync 必须分开理解;② BGSAVE 不阻塞式生成文件,却仍有 fork、COW、内存和磁盘竞争;③ AOF 重写基于当前内存状态生成最小表示,不是逐行压缩旧日志;④ 持久化只能解决单节点恢复的一部分问题,副本、异地备份、恢复演练和业务补偿缺一不可。

下一篇,我们继续追问"数据留在内存里时,Redis 怎样决定它什么时候消失"------《Redis 06 · 内存管理:过期删除、淘汰策略与大 Key 治理》。TTL 到期为什么不会立刻删除?内存满了以后 LRU、LFU 和随机淘汰怎么选?BigKey、HotKey 和内存碎片又怎样把一个看似健康的实例拖慢?

相关推荐
curd_boy2 小时前
【Redis】Redis从缓存到AI向量平台
人工智能·redis·缓存
kiracrimson3 小时前
从缓存的角度看链表与线性表的差异
数据结构·链表·缓存
莫得感情 o3 小时前
Redis 02 · 数据结构:String 到 Stream 怎么选
redis
devpotato4 小时前
缓存与数据库更新顺序不一致问题
java·数据库·redis
2601_962301016 小时前
深入理解缓存(Cache):原理、应用与优化
缓存·计算机原理·优化策略·数据访问·系统性能
数智启示录7 小时前
PostgreSQL 计划缓存实战(第 10 篇):预编译 SQL 前五次都快,第六次为什么可能变慢
经验分享·sql·缓存·postgresql·面试
NeverFear7929 小时前
【黑马点评】短信登录
java·redis·tomcat·intellij-idea
攻城有术10 小时前
专项攻克——重写 Redis 依赖包方法的 6 种实现方式
java·数据库·redis·bootstrap
跨境数据猎手10 小时前
京东开放平台商品详情接口(jd.item_get):签名、缓存与批量采集
java·spring·缓存