Redis AOF 日志:宕机了,如何避免数据丢失?

Redis 的数据主要保存在内存中。进程退出、操作系统崩溃或机器断电后,只靠内存无法恢复数据。

AOF(Append Only File)通过持续记录会改变数据集的操作,让 Redis 重启时能够重新构造数据。不过,AOF 并不等于"绝不丢数据"。它真正解决的是:把内存中的状态转换为可以落盘、重放和恢复的持久化记录,并允许使用者在性能与耐久性之间选择。

一、AOF 记录的是什么

1. 只记录会改变数据集的操作

AOF 不会把所有客户端请求都保存下来。GET、EXISTS 这类只读命令没有必要参与数据恢复;SET、HSET、INCR、DEL 等会改变数据状态的操作才需要被持久化。

Redis 记录的是可重放的命令效果,但日志中的命令不一定与客户端发送的原始文本完全相同。Redis 可能为了确定性、过期处理或内部传播语义,将操作转换成等价的可重放命令。

2. AOF 使用 RESP 编码

假设执行:

text 复制代码
SET testkey testvalue

对应的 RESP2 编码类似:

text 复制代码
*3
$3
SET
$7
testkey
$9
testvalue

其中:

  • *3 表示数组中有三个元素。
  • $3、$7、$9 表示后续字符串的字节长度。
  • AOF 可以按 Redis 协议解析并重放,而不是依赖自然语言形式的日志。

3. AOF 是写后日志,但不能误解为"完全不阻塞"

Redis 会先校验并执行命令,成功修改内存数据后,再把需要持久化的效果追加到 AOF 缓冲区。执行失败的命令不会作为有效数据变更写入 AOF,因此恢复时不会重放一条本来就无法成功执行的命令。

"写后日志"只表示日志生成发生在命令执行之后,不表示 AOF I/O 永远不会影响当前请求:

  • appendfsync always 会在回复客户端之前等待 AOF 写入和 fsync 完成。
  • appendfsync everysec 的 fsync 通常由后台线程执行,但主线程的 write(2) 仍可能因磁盘或内核回写压力而阻塞。
  • AOF 文件切换、fork 和写时复制也可能造成延迟尖峰。

二、从执行成功到真正落盘,中间有四层

一条写命令成功执行后,并不会瞬间到达持久化介质。完整链路可以拆成四层:

  1. 内存数据集:命令已经改变 Redis 中的数据。
  2. Redis AOF 缓冲区:命令的持久化表示等待写入文件。
  3. 操作系统页缓存 :write(2) 已经把数据交给内核,但数据未必到达磁盘。
  4. 持久化介质 :fsync 或 fdatasync 完成后,Redis 才能按当前硬件和文件系统提供的语义认为数据已持久化。

不同故障会影响不同层:

故障类型 可能丢失的内容 AOF 能否单独解决
Redis 进程崩溃,操作系统仍正常 尚未写入内核的 AOF 缓冲区 取决于写回策略和当时状态
操作系统崩溃或机器断电 尚未完成 fsync 的页缓存数据 取决于 appendfsync 策略
磁盘损坏、云盘丢失 本机全部持久化文件 不能,需要副本和异机备份
误执行 FLUSHALL、错误业务写入 正确数据被合法命令覆盖或删除 AOF 会忠实记录错误,需要备份或时间点恢复方案

所以,AOF 提升的是单实例本地持久化能力,不等于备份,也不等于跨节点强一致。

三、三种 appendfsync 策略

启用 AOF:

conf 复制代码
appendonly yes

Redis 配置文件默认是 appendonly no,也就是默认不启用 AOF。启用后,appendfsync 决定 Redis 主动要求操作系统把 AOF 数据同步到持久化介质的频率。

1. appendfsync always

conf 复制代码
appendfsync always

每批新 AOF 数据写入后都执行 fsync,完成后才回复客户端。多个并发请求或 Pipeline 中的一批命令可能共享一次写入和 fsync,但它仍然是三种策略中延迟最高的一种。

适合:单条已确认写入的丢失成本很高,并且能够接受磁盘同步延迟的场景。

边界:它只能确认本机 AOF 已按系统语义落盘,不能防止磁盘整体损坏,也不能自动保证故障转移后的新主节点一定包含该写入。

2. appendfsync everysec

conf 复制代码
appendfsync everysec

这是 appendfsync 的默认值,也是最常见的折中方案。Redis 通常每秒由后台 I/O 线程执行一次 fsync。发生机器级故障时,通常可能丢失最近约一秒的数据;遇到严重磁盘阻塞或调度延迟时,不应把"一秒"理解成绝对不变的硬上限。

主线程仍负责把 AOF 缓冲区通过 write(2) 交给内核。当上一次后台 fsync 长时间未结束时,Redis 会暂缓部分写入,但最终仍可能执行会阻塞的 write(2),形成延迟尖峰。

3. appendfsync no

conf 复制代码
appendfsync no

Redis 仍然执行 write(2),但不主动调用 fsync,何时落盘由操作系统决定。它减少了 Redis 主动同步磁盘的开销,但数据丢失窗口受内核参数、文件系统和 I/O 压力影响,不能简单认为一定是某个固定秒数。

适合:数据可以从其他系统重建,或者业务更重视吞吐和延迟、能够接受更大丢失窗口的场景。

4. 策略不是只看平均吞吐

选择时至少需要同时观察:

  • 平均延迟与 P99、P999 尾延迟。
  • 故障时最多允许丢失多少已确认写入。
  • 磁盘 fsync 延迟是否稳定。
  • 是否有副本、备份和故障转移。
  • AOF 重写期间是否出现 I/O 竞争。

四、为什么 AOF 文件需要重写

AOF 按操作追加。同一个 Key 被修改一千次,日志中可能存在一千次历史操作,但恢复最新状态往往只需要少量命令。

例如:

text 复制代码
SET counter 1
INCR counter
INCR counter
INCR counter

如果当前最终值为 4,新 AOF 只要能重建 counter = 4 即可,无须保留全部历史过程。

AOF 重写不是压缩旧文件,也不是逐行合并旧命令。它读取当前内存数据集,为当前状态生成一份新的基础表示。因此,重写可以:

  1. 删除已经失效的历史操作。
  2. 减小持久化文件体积。
  3. 减少重启时需要加载和重放的数据量。
  4. 控制长期运行实例的恢复时间。

自动重写常见配置为:

conf 复制代码
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

含义是:AOF 至少达到 64 MB,并且相对上次重写后的基准大小增长 100% 时,可以触发自动重写。真实生产参数应根据写入速度、磁盘空间和可接受的重写频率调整。

五、AOF 重写不是后台线程,而是子进程

BGREWRITEAOF 的命令复杂度标记为 O(1),指的是发起动作本身,不表示整个重写只做常数工作。真正的重写需要遍历数据集并生成持久化文件。

Redis 在类 Unix 系统上通过 fork 创建子进程,由子进程执行重写。父进程继续处理客户端请求。

这里必须区分:

  • 后台 I/O 线程可以承担 fsync、异步释放等任务。
  • AOF 重写主体由子进程完成,不是普通后台线程。
  • fork 发生在主线程,创建子进程的瞬间仍可能让事件循环停顿。

子进程最初通过写时复制(Copy-on-Write,COW)与父进程共享内存页。重写期间父进程继续修改数据时,相应内存页会被复制,导致额外内存占用和内存带宽开销。

六、Redis 7 之前与 Redis 7+ 的重写模型

1. Redis 7 之前:单文件 AOF 与重写差异缓冲区

旧版模型可以概括为:

  1. 子进程根据 fork 时刻的数据集生成临时新 AOF。
  2. 父进程继续把新写入追加到正在使用的旧 AOF。
  3. 同期的新写入还会进入 AOF 重写差异缓冲区。
  4. 子进程主体完成后,父进程把差异追加到新 AOF,再原子替换旧文件。

原文常说的"一个拷贝、两处日志"描述的是这一代实现。它的核心目标是在重写期间继续服务,同时让新 AOF 包含重写期间发生的变更。

2. Redis 7+:多部件 AOF

从 Redis 7.0 开始,AOF 由多类文件共同组成:

  • BASE 文件:最近一次成功重写生成的基础数据,可以是 RDB 或 AOF 格式。
  • INCR 文件:BASE 之后发生的增量写命令,可以存在多个。
  • manifest 文件:记录有效 BASE、INCR 文件及加载顺序。

重写开始时,父进程直接打开一个新的 INCR 文件继续记录后续写入;子进程根据当时的数据集生成新的 BASE 文件。完成后,Redis 生成临时 manifest,并通过原子替换让新 BASE 与相应 INCR 生效。

这种设计不再需要旧版 AOF 重写差异缓冲区。INFO persistence 中的 aof_rewrite_buffer_length 也从 Redis 7.0 起被移除。

3. 为什么 BASE 文件通常是 RDB 格式

现代 Redis 默认配置:

conf 复制代码
aof-use-rdb-preamble yes

这会让 BASE 使用紧凑的 RDB 二进制表示,后续 INCR 继续记录 AOF 命令。好处是基础数据加载更快、体积通常更小,同时保留增量 AOF 的耐久性。

因此,看到 .base.rdb 并不表示 AOF 被关闭了;它可能正是 Redis 7+ 多部件 AOF 的基础文件。

七、重写"后台执行"为什么仍可能造成阻塞

重写主体虽然在子进程中运行,但父进程仍可能受到影响:

1. fork 停顿

fork 需要创建进程并复制页表。实例内存越大、页表越大或虚拟化环境越差,停顿越明显。LATENCY 监控中的 fork 事件可以帮助定位这类尖峰。

2. 写时复制内存

重写期间写流量越大,被修改的内存页越多,COW 额外内存越高。内存不足可能触发交换、OOM 或重写失败。

3. 磁盘 I/O 竞争

子进程持续写 BASE 文件,父进程还要写 INCR AOF、执行 fsync。两者竞争同一磁盘时,write(2) 和 fsync 延迟都可能升高。

默认开启的配置:

conf 复制代码
aof-rewrite-incremental-fsync yes

会让重写子进程每生成约 4 MB 数据就逐步同步一次,避免最后一次性同步巨量数据造成更大的延迟尖峰。

4. no-appendfsync-on-rewrite 的耐久性代价

conf 复制代码
no-appendfsync-on-rewrite no

默认值 no 表示重写期间仍遵守正常 appendfsync 语义。改成 yes 可以降低重写期间的磁盘同步竞争,但这段时间的耐久性会接近 appendfsync no,故障时可能丢失更多数据。

它不是免费的性能开关,必须按照允许的数据丢失窗口决定。

八、Redis 如何从 AOF 恢复

1. 启动加载顺序

Redis 7+ 启动时读取 manifest,先加载 BASE,再按顺序加载有效的 INCR 文件,最终还原数据集。

如果 RDB 和 AOF 同时开启,Redis 启动时优先使用 AOF,因为它通常包含比周期性 RDB 快照更新的数据。

2. 尾部截断与中间损坏不是一回事

机器故障可能让 AOF 最后一条命令只写了一部分。默认配置:

conf 复制代码
aof-load-truncated yes

允许 Redis 在遇到尾部不完整记录时,加载前面完整的数据并启动,同时输出警告。

如果损坏发生在文件中间,Redis 通常会拒绝继续加载,因为无法可靠判断后续记录边界和数据正确性。此时应先复制原文件,再使用匹配 Redis 版本的 redis-check-aof 检查;--fix 会删除无法恢复的尾部内容,不能在没有备份的情况下盲目执行。

3. 恢复速度仍受数据量限制

AOF 能恢复数据,但不保证恢复很快。BASE 越大、INCR 越多,加载时间越长。控制重写频率、磁盘吞吐和文件数量,实际上是在控制恢复时间目标(RTO)。

九、关键写入如何获得更明确的持久化确认

Redis 7.2 起提供:

text 复制代码
WAITAOF numlocal numreplicas timeout

例如:

text 复制代码
SET order:1001 paid
WAITAOF 1 1 2000

它会等待当前连接之前的写入,被本地主节点以及指定数量的副本 fsync 到各自 AOF,或者等待超时。返回值包含实际完成本地和副本 fsync 的数量,客户端必须检查结果是否达到要求。

WAITAOF 可以提高关键操作的真实数据安全性,但仍不能把 Redis 变成严格的强一致数据库:超时、故障转移选择、未覆盖的节点以及整个故障域损坏仍可能造成数据缺失。

十、生产环境如何检查 AOF

1. 检查实际配置

text 复制代码
CONFIG GET appendonly
CONFIG GET appendfsync
CONFIG GET no-appendfsync-on-rewrite
CONFIG GET auto-aof-rewrite-*

不要只看配置文件。启动参数、容器挂载、运行时 CONFIG SET 和托管服务控制面都可能改变最终生效值。

2. 检查持久化状态

text 复制代码
INFO persistence

重点关注:

  • aof_enabled:AOF 是否启用。
  • aof_rewrite_in_progress:是否正在重写。
  • aof_rewrite_scheduled:是否等待其他持久化任务结束后重写。
  • aof_last_bgrewrite_status:上次重写是否成功。
  • aof_last_write_status:最近一次 AOF 写入是否成功。
  • aof_current_size 与 aof_base_size:当前规模与重写基准。
  • aof_pending_bio_fsync:后台队列中待处理的 fsync 数量。
  • aof_delayed_fsync:被延迟的 fsync 次数。
  • aof_last_cow_size:上次 AOF 重写产生的 COW 内存规模。

3. 检查延迟来源

先设置非零阈值,再观察:

text 复制代码
CONFIG SET latency-monitor-threshold 10
LATENCY LATEST
LATENCY DOCTOR

AOF 相关事件包括 aof-write、aof-write-pending-fsync、aof-fsync-always、aof-rename 和 fork。还应结合 iostat、磁盘队列、fsync 延迟、内存余量和交换活动判断。

十一、AOF 上线检查清单与答案

1. 只设置 appendfsync everysec 就已经启用 AOF 了吗?

**没有。**还必须启用 appendonly yes。appendfsync 只是 AOF 启用后的同步策略。

2. 命令返回成功是否一定表示已经落盘?

不一定。 always 会在回复前等待当前批次完成 fsync;everysec 和 no 下,回复时数据可能仍只位于 AOF 缓冲区或操作系统页缓存。

3. everysec 是否绝对最多丢一秒?

**不能把它当作数学上的硬上限。**正常情况下目标是约一秒,但严重 I/O 阻塞、线程调度和系统故障时,实际边界取决于最后一次成功 fsync。

4. AOF 重写是否由后台线程完成?

**不是。**主体由 fork 出的子进程完成。后台线程主要参与 fsync 等任务。

5. Redis 7+ 是否仍只有一个 appendonly.aof 文件?

**不是。**现代 Redis 使用 BASE、一个或多个 INCR,以及 manifest 组成多部件 AOF,并保存在 appenddirname 指定的目录中。

6. 重写期间主线程是否完全不会受影响?

不会完全隔离。 fork、COW、磁盘竞争、内存压力和最终文件切换都可能影响延迟。

7. AOF 能否代替备份和副本?

**不能。**本机磁盘损坏、误操作或整个故障域丢失时,本地 AOF 可能一起失效。AOF、复制、异机备份解决的是不同问题。

8. 备份 Redis 7+ 的 AOF 是否只复制当前 .aof 文件?

**不是。**需要一致地备份 appenddirname 中由 manifest 引用的整组文件。复制期间若恰好发生重写,可能得到不一致的文件组合,因此必须按官方流程暂停自动重写并确认没有重写进行中,或使用能够保证一致性的备份方案。

对于 Redis 7.0--8.8,手工在线复制前通常需要把 auto-aof-rewrite-percentage 暂时设为 0,确认 aof_rewrite_in_progress 为 0,复制完整目录后再恢复原配置。

9. Redis 8.10+ 是否仍要手工暂停重写才能做在线备份?

**不一定。**Redis 8.10 起提供 BACKUP START、BACKUP LIST、BACKUP SEAL、BACKUP CLEANUP 等命令,可以在继续处理写入的同时生成一套自包含、可恢复的 BASE、INCR 与 manifest 文件。备份完成后应复制 BACKUP LIST 返回的密封文件,并在确认远端文件完整后执行清理。

这套命令解决的是一致备份流程,不会自动替你完成异地传输、保留周期、校验和恢复演练。

十二、核心结论

Redis AOF 的完整逻辑可以概括为:

  1. 成功执行会改变数据集的命令。
  2. 把可重放的效果追加到 AOF 缓冲区。
  3. 通过 write(2) 写入操作系统页缓存。
  4. 按 appendfsync 策略执行 fsync。
  5. 文件增长后,通过子进程重写基础数据。
  6. Redis 7+ 用 BASE、INCR 和 manifest 管理多部件 AOF。
  7. 重启时按 manifest 顺序加载并重建内存数据集。

AOF 的价值不是消灭所有故障,而是提供一个可配置的耐久性边界。真正可靠的方案还需要把 appendfsync、磁盘能力、重写开销、复制、WAITAOF、备份和恢复演练放在同一个设计中考虑。

相关推荐
神仙别闹1 分钟前
基于C++实现的贪吃蛇游戏平台
java·c++·游戏
布吉岛的石头10 分钟前
Java 程序员第 49 阶段11:Java 接 BERT:用 ONNX Runtime 做句向量推理
java·人工智能·python·深度学习·bert·transformer
niucloud-admin19 分钟前
JAVA V6 多商户商城 开发文档——JAVA服务端
java·开发语言
fengkai454522 分钟前
十二、Redis -2
运维·数据库·redis·容器
hweiyu0027 分钟前
Redis命令:BLPOP
redis·缓存
alexlifexyz33 分钟前
写了个 Claude Code 插件:AI 写的 Java 违反阿里规约,就写不进文件
java
威哥爱编程1 小时前
【AI全栈12-01】Spring Boot 为何是 AI 全栈后端优选
java·spring boot·后端
威哥爱编程1 小时前
【AI全栈12-03】Spring Boot 用多模型路由把智能客服成本降下来
java·spring boot·后端
威哥爱编程1 小时前
【AI全栈12-02】Spring Boot 跑通第一个 AI 对话接口:HR 政策问答机器人实战
java·spring boot·后端
Wang's Blog1 小时前
Java 中间件之 RabbitMQ 快速入门: 异步通讯的优缺点
java·中间件·java-rabbitmq