☰
Redis AOF持久化实战:刷盘策略、重写机制与故障修复
2026/10/26 3:21:48 网站建设 项目流程

Redis 的持久化方案里,RDB 和 AOF 是绕不开的两条路。很多人第一次接触 AOF,是因为遇到了一件很具体的事:机器断电或者进程被强杀之后,重启发现数据少了一截,而 RDB 的落盘间隔又太长,兜不住这段丢失。这时候翻文档看到 AOF,说它能"更实时地记录写操作",于是打开配置就上了。但真跑起来才发现,AOF 不是开个开关就完事的东西——刷盘策略选错会拖慢写入,重写时机没控好会撑爆磁盘,文件损坏了不会修就只能干瞪眼。

这篇内容就围绕 AOF 的完整使用链路来讲:它到底怎么记录、三种刷盘策略差在哪、重写机制什么时候触发、损坏文件怎么救、以及生产环境里那些文档不会明说但踩过就忘不掉的细节。适合已经会用 Redis 基本命令、但对持久化机制还停留在"知道有这回事"阶段的开发者,也适合正在做数据可靠性方案选型、需要判断 AOF 到底能不能扛住自己业务场景的同学。下面这些内容都是围绕实际配置和运维场景展开的,能直接对照着改配置、跑命令。

1. AOF 到底在磁盘上写了什么

要理解 AOF 的所有行为,得先搞清楚它落盘的内容长什么样。这件事想明白了,后面刷盘策略、重写、修复这些操作背后的逻辑就都顺了。

1.1 从一条 SET 命令看 AOF 的记录格式

AOF 全称 Append Only File,追加写文件。它的核心思路非常朴素:把每一条会修改数据的写命令,按执行的先后顺序,以 Redis 协议格式追加到文件末尾。注意这里的关键词是"写命令"——读命令不记录,因为读操作不改变数据状态,记了也没用。

举个具体的例子。客户端执行:

SET user:1001 "张三"

这条命令进到 AOF 文件里,不是原样存成SET user:1001 "张三"这么一行文本,而是被转成 Redis 自己的 RESP 协议格式。实际落盘大概长这样(\r\n是协议规定的分隔符):

*3\r\n$3\r\nSET\r\n$9\r\nuser:1001\r\n$6\r\n张三\r\n

拆开看:*3表示这条命令有 3 个参数(SET、key、value),每个参数前面$数字表示该参数的长度,后面跟实际内容。这种格式的好处是解析极其简单快速,Redis 重启加载时按协议逐条读出来重新执行就行,不需要做复杂的语法分析。

为什么不用人类可读的纯文本?因为 RESP 格式对机器友好,加载速度快,而且能精确处理二进制安全的内容——比如 value 里带空格、带换行、带特殊字符,纯文本格式很容易解析歧义,RESP 用长度前缀就完全规避了这个问题。这是设计上的取舍,牺牲了一点可读性,换来的是加载效率和正确性。

1.2 命令追加发生在什么时候

这里有个容易混淆的点:命令是先写进 AOF 缓冲区,还是直接写磁盘?答案是先写缓冲区。完整链路是这样的:

  1. 客户端发来写命令
  2. Redis 执行命令,修改内存数据
  3. 执行成功后,把这条命令按 RESP 格式追加到aof_buf(AOF 缓冲区,在内存里)
  4. 根据配置的刷盘策略,在某个时机把aof_buf的内容写到磁盘文件

第 3 步和第 4 步是分开的,这个分离设计是理解 AOF 性能特性的关键。命令执行完就返回给客户端了,但此时数据可能还在内存缓冲区里没落盘。如果这时候机器断电,缓冲区里没刷下去的部分就丢了。丢多少,取决于第 4 步的刷盘策略——这就是下一节要展开的内容。

注意:AOF 记录的是命令执行"之后"的追加动作,但命令本身是先执行再记录的。这意味着如果命令执行到一半进程崩了,可能出现内存改了但 AOF 没记的情况。不过 Redis 的单线程模型保证了单条命令的原子性,所以不会出现"半条命令"被记录的问题。

1.3 AOF 文件的结构组成

一个完整的 AOF 文件不是从头到尾只有命令,它由几部分拼起来:

  • 命令追加区:主体部分,一条条 RESP 格式的写命令
  • 重写产生的头部:如果经历过 AOF 重写,文件开头可能有一段由重写生成的紧凑命令序列(本质还是命令,只是把冗余操作合并了)
  • 可能存在的 RDB 混合格式:较新版本支持 AOF 与 RDB 混合持久化,重写时前半段用 RDB 格式存快照,后半段追加增量命令

混合格式这个点值得单独说一句。纯 AOF 重写出来的文件,本质还是命令序列,加载时要一条条重放,数据量大时加载慢。混合格式把重写那一刻的全量数据用 RDB 二进制格式存下来,加载时先快速载入 RDB 部分,再重放后面的增量命令,启动速度能快一个量级。这个特性在配置里通过aof-use-rdb-preamble控制,默认在新版本里是开启的。

2. 三种刷盘策略:可靠性、性能与丢失窗口的三角权衡

appendfsync这个配置项是 AOF 使用中最重要的一个决策点,它直接决定了"断电会丢多少数据"和"写入性能损耗多大"这两件事。三个可选值各有明确的适用场景,选错方向要么丢数据,要么拖垮性能。

2.1 always:每条命令都落盘,最稳也最慢

配置成appendfsync always,意思是每追加一条命令到缓冲区后,立刻调用 fsync 把缓冲区刷到磁盘,刷完才继续处理下一条命令。

可靠性上这是天花板:理论上最多丢一条正在处理、还没完成 fsync 的命令。但代价很直接——每条写命令都要等一次磁盘 fsync。机械盘上单次 fsync 是毫秒级,SSD 上快一些但也不是免费的。这意味着写入吞吐会被 fsync 的延迟死死卡住,QPS 上不去。

我实测过一个场景,同样的硬件,always相比everysec,纯写入压测的吞吐差了将近一个数量级。所以这个策略只适合那种"一条数据都不能丢"且写入量本身不大的场景,比如某些金融流水记录、关键配置变更。写入量大的业务用它,基本等于给自己上刑。

2.2 everysec:每秒刷一次,绝大多数场景的默认答案

appendfsync everysec是 Redis 的默认值,也是生产环境里用得最多的。它的机制是:后台有个专门的刷盘线程(或者主线程在时间事件里),每隔大约 1 秒把aof_buf里的内容 fsync 到磁盘一次。

丢失窗口很明确:最坏情况丢 1 秒左右的写命令。对绝大多数业务来说,这个窗口是可以接受的——毕竟很多业务本身就有重试和补偿机制,丢 1 秒的数据不至于造成灾难。

性能上它把 fsync 从"每条一次"降到"每秒一次",写入吞吐基本不受 fsync 拖累,只在那一秒的刷盘瞬间有极小的抖动。这是可靠性和性能之间最平衡的点,也是为什么它被设为默认。

有个细节要注意:everysec模式下,如果上一次 fsync 还没完成,主线程不会傻等,而是继续往缓冲区写。如果磁盘特别慢,fsync 积压,Redis 会在主线程里做阻塞式 fsync 来兜底,这时候会出现明显的延迟毛刺。所以用everysec也要关注磁盘的 fsync 能力,别用那种 IO 抖动严重的盘。

2.3 no:交给操作系统,性能最好但丢失窗口不可控

appendfsync no的意思是 Redis 自己不主动 fsync,只把数据写到操作系统的页缓存(page cache),什么时候真正落盘由操作系统决定,通常 Linux 是 30 秒左右刷一次脏页。

这个策略下 Redis 的写入几乎不受磁盘影响,性能最好。但丢失窗口完全不可控——操作系统崩了或者断电,最多可能丢 30 秒的数据。而且这个 30 秒是系统行为,Redis 管不了。

它适合什么场景?数据能从别处重建、或者对丢失不敏感的缓存类数据。如果你的 Redis 纯粹当缓存用,丢了从数据库回源就行,那no是可以考虑的。但只要数据有"唯一真相"的属性,就别碰这个选项。

三种策略的对比整理成表更直观:

策略丢失窗口写入性能适用场景
always最多 1 条命令最差,受 fsync 延迟限制极少数不能丢的数据,写入量小
everysec约 1 秒好,接近无持久化绝大多数生产场景的默认选择
no不可控,可能数十秒最好纯缓存、可重建数据

2.4 怎么根据业务选:一个判断框架

选策略不要拍脑袋,按两个维度问自己:数据丢了能不能恢复,写入量有多大。

数据能从上游重建(缓存场景),选no或everysec都行,看你对性能的渴求程度。数据是唯一来源、丢了就是事故,那必须everysec起步,写入量小且对丢失零容忍才考虑always。写入量巨大又要求零丢失,那 Redis 单机 AOF 可能不是最优解,得考虑主从加多副本、或者换更适合的存储方案。

实际配置就一行,在redis.conf里:

appendonly yes appendfsync everysec

改完重启或者用CONFIG SET appendfsync everysec动态生效(注意appendonly的动态开启在部分版本行为有差异,稳妥起见改配置文件重启)。

3. AOF 重写:为什么文件会越写越大,以及怎么收住

AOF 是追加写,这个特性决定了文件只会变大不会变小。一条 key 被反复 SET 一百次,AOF 里就有一百条 SET 记录,但实际有效数据只有最后一次。文件膨胀到几个 G 甚至几十 G,不仅占磁盘,重启加载时重放这些冗余命令也慢得离谱。AOF 重写就是来解决这个问题的。

3.1 重写的本质:用当前数据状态生成最小命令集

重写不是去读旧 AOF 文件做压缩,而是直接读当前内存里的数据,为每个 key 生成一条能重建它当前状态的最简命令。比如一个 list 经过多次 RPUSH 变成了 100 个元素,重写时不会保留 100 条 RPUSH,而是生成一条RPUSH key v1 v2 ... v100(或者分批次),把当前状态一次性表达出来。

这样重写出来的新文件,体积通常比旧文件小得多,而且只包含"重建当前状态所必需"的命令,没有任何历史冗余。加载时重放这些命令,得到的就是重写那一刻的数据快照。

3.2 触发时机:两个参数怎么配合

重写不是随时都在跑,它由两个配置项控制触发条件:

auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb

含义是:当 AOF 文件大小比"上一次重写后的基准大小"增长了 100%(也就是翻倍),并且文件绝对大小超过 64MB 时,才触发自动重写。两个条件是"与"的关系,缺一不可。

为什么要两个条件?min-size防止小文件频繁重写——一个才几 MB 的文件,翻倍也就十几 MB,重写一次的开销可能比收益还大。percentage则控制增长比例,避免文件无限膨胀。这两个值需要根据业务写入量调,写入特别猛的业务,percentage可以调低到 50 甚至更低,让重写更早介入;写入平缓的,保持默认或调高都行。

手动触发用命令:

BGREWRITEAOF

这个命令是后台异步执行的,不会阻塞主线程处理请求。

3.3 重写期间的新写入去哪了:AOF 重写缓冲区

这是重写机制里最精妙的设计,也是面试常问、实际排错常踩的点。重写是后台子进程干的活,它读的是重写开始那一刻的内存快照。但重写过程中,主线程还在处理新的写命令,这些新命令如果只写进旧 AOF 文件,那重写完成后新文件里就缺了这段时间的数据。

Redis 的解法是引入AOF 重写缓冲区(aof_rewrite_buf)。重写期间:

  1. 新的写命令同时追加到旧 AOF 文件和重写缓冲区
  2. 子进程把内存快照写成新的 AOF 文件(写到临时文件)
  3. 子进程写完后,主线程把重写缓冲区里的内容追加到新文件末尾
  4. 用新文件原子替换旧文件

这样新文件 = 重写时刻的全量快照 + 重写期间的所有增量,数据完整。这个设计保证了重写不丢数据,代价是重写期间内存里要多维护一份重写缓冲区,写入量大的时候这块内存会涨,需要留意。

提示:重写期间如果写入量特别大,重写缓冲区可能撑大内存。可以观察INFO persistence里的aof_rewrite_buffer_length相关指标,异常增长说明重写跟不上写入速度,考虑调大重写触发阈值或错峰执行。

3.4 重写带来的阻塞风险与规避

虽然重写是子进程异步做的,但有两个环节可能影响主线程:

一是fork 子进程的瞬间。fork 本身要复制页表,内存越大耗时越长,这个瞬间主线程是阻塞的。几十 G 的实例 fork 一次可能阻塞几百毫秒甚至更久,表现为请求延迟毛刺。规避办法是控制单实例内存不要太大,或者用主从架构在从库上做重写。

二是重写缓冲区追加到新文件的最后阶段。这个阶段主线程要把缓冲区内容写进新文件,如果缓冲区很大,这一步也会阻塞。所以重写期间控制写入量、避免缓冲区暴涨很重要。

实际运维里,我一般会把自动重写的触发阈值调得比默认激进一点,让每次重写的数据量小一些、频率高一些,单次 fork 和收尾的阻塞就小。用"小步快跑"换"平稳",比攒一个巨大文件一次重写要安全。

4. AOF 文件损坏了怎么救:从报错到恢复的完整链路

AOF 最让人头疼的场景之一,是重启时加载失败,日志里甩出一句Bad file format reading the append only file。这时候别慌,先搞清楚损坏发生在哪、能救回多少,再决定怎么处理。

4.1 先判断:是尾部截断还是中间损坏

AOF 是追加写,最常见的损坏是尾部不完整——比如写到最后一条命令时机器断电,文件末尾留了半条命令。这种损坏相对好处理,因为前面的数据都是完整的。

判断方法:用redis-check-aof工具检查(不带--fix):

redis-check-aof /path/to/appendonly.aof

它会扫描文件,报告是否能正常解析、以及如果发现问题出在哪个位置。如果报的是文件末尾有截断或不完整命令,那就是尾部损坏,可以修。如果报的是中间某处格式错误,那可能是更严重的问题(比如磁盘坏块),修复要谨慎,因为中间损坏意味着损坏点之后的数据可能也不可信。

4.2 用 redis-check-aof --fix 修复尾部截断

确认是尾部截断后,用修复模式:

redis-check-aof --fix /path/to/appendonly.aof

这个命令会扫描文件,找到第一条不完整的命令,把从那里到文件末尾的内容全部截掉,只保留前面完整可解析的部分。执行时会提示你确认,输入y继续。

修复的本质是"丢弃不完整的尾巴,保住完整的前半段"。所以修复后你会丢掉最后那部分没写完的数据,但前面的数据是安全的。修复前务必备份原文件:

cp appendonly.aof appendonly.aof.bak

这个备份动作千万别省。修复工具虽然可靠,但万一操作失误或者文件情况特殊,有备份才有退路。

4.3 混合持久化格式下的修复注意点

如果开了aof-use-rdb-preamble(混合格式),AOF 文件前半段是 RDB 二进制、后半段是命令。这种情况下redis-check-aof的修复逻辑依然适用,但要注意:RDB 部分如果损坏,修复工具处理起来更棘手,因为它不是命令序列,没法简单截断。

混合格式下如果 RDB 头部损坏,通常意味着重写过程出了问题,这种情况建议直接从备份恢复,或者用 RDB 快照重建,别硬修。所以开了混合持久化,备份策略要跟上,不能只依赖 AOF 自身的修复能力。

4.4 修复后验证:别修完就直接上生产

修复完不是终点,得验证数据完整性。稳妥的做法是:

  1. 把修复后的文件放到一个测试实例上加载,确认能正常启动
  2. 抽查关键 key 是否存在、值是否正确
  3. 对比修复前后的数据量(用DBSIZE或抽样),确认丢失范围符合预期(应该只丢尾部那部分)

确认无误后再替换生产文件。直接拿修复文件上生产,万一还有隐藏问题,二次故障的排查成本更高。

5. 生产环境里那些配置文档不会写的细节

前面讲的都是机制和标准操作,这一节聊点实际运维中积累的经验,都是文档里不会明说、但踩过就记住的东西。

5.1 appendfsync everysec 下的延迟毛刺从哪来

用everysec的时候,偶尔会看到请求延迟突然飙一下,几十毫秒甚至上百毫秒。很多人第一反应是网络或者慢查询,其实很可能是 AOF 的 fsync 在捣鬼。

前面提过,everysec下如果上一次 fsync 还没完成,主线程会阻塞等待。磁盘 IO 抖动(比如同机器上别的进程在狂写、或者云盘的 IO 配额被打满)会导致 fsync 变慢,进而拖住主线程。排查这类毛刺,要看INFO persistence里的aof_delayed_fsync计数——这个值持续增长,说明 fsync 延迟已经在影响主线程了。

应对办法:把 AOF 文件放到独立的、IO 能力有保障的盘上,别和 RDB、日志、其他高 IO 进程抢盘。云环境下关注磁盘的 IOPS 和吞吐配额,别让 AOF 的 fsync 排在别的 IO 后面。

5.2 AOF 和 RDB 要不要同时开

这个问题没有标准答案,取决于你要什么。两个都开,重启时 Redis 优先用 AOF 恢复(因为 AOF 通常更完整),RDB 作为额外备份。代价是两份持久化开销,磁盘占用翻倍,fork 也更频繁。

我的经验是:以 AOF 为主做数据可靠性,RDB 保留但降低频率做冷备份。比如 AOF 用everysec保证实时性,RDB 配置成几小时一次甚至一天一次,纯粹作为"万一 AOF 彻底坏了"的兜底。这样既保证了恢复点目标,又不会让 RDB 的 fork 频繁干扰主线程。

如果业务对恢复速度要求极高、数据量又大,纯 AOF 加载慢的问题可以用混合持久化缓解,不必为了加载速度强行依赖 RDB。

5.3 磁盘空间规划:别等写满了才想起来

AOF 文件会持续增长,重写时还要额外空间写临时文件(新文件写完才替换旧的,所以峰值需要约两倍空间)。磁盘规划要按"当前 AOF 大小 × 2 + 余量"来算,别卡着实际大小配盘。

另外要监控磁盘使用率,设个阈值告警(比如 80%)。AOF 写满磁盘的后果很严重:Redis 会拒绝所有写命令(取决于stop-writes-on-bgsave-error相关配置和具体版本行为),业务直接受影响。提前告警比事后救火强得多。

5.4 重写频率和实例内存大小的关系

前面提过 fork 阻塞和内存大小正相关。单实例内存越大,fork 越慢,重写期间的重写缓冲区也越大。所以大内存实例要更谨慎地控制重写频率,或者干脆用主从架构,把重写放到从库上做,主库只负责写。

一个实用的经验值:单实例内存控制在十几 G 以内,fork 阻塞通常在可接受范围;超过这个量级,就要认真评估 fork 对延迟的影响,必要时拆分实例或调整架构。

5.5 监控指标清单

AOF 相关的监控,除了常规的内存、QPS,重点盯这几个:

指标含义异常信号
aof_current_size当前 AOF 文件大小持续增长不回落,说明重写没生效
aof_base_size上次重写后的基准大小与 current 比值接近触发阈值要留意
aof_delayed_fsyncfsync 延迟次数持续增长说明磁盘 IO 跟不上
aof_rewrite_in_progress是否正在重写长期为 1 说明重写卡住
aof_last_bgrewrite_status上次重写结果为 err 要立即排查

这些指标通过INFO persistence都能拿到,接到监控系统里设好阈值,比出事后翻日志高效得多。

6. 一个完整的 AOF 配置落地示例

把前面的内容串起来,给一份可以直接参考的配置和操作流程。假设场景是:单实例 Redis,数据是业务主数据不能丢,写入量中等,机器是 SSD。

配置文件关键项:

# 开启 AOF appendonly yes # 刷盘策略,兼顾可靠性和性能 appendfsync everysec # 混合持久化,加快重启加载 aof-use-rdb-preamble yes # 重写触发:增长 100% 且超过 64MB auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb # 重写期间不阻塞 fsync(视版本和场景调整) no-appendfsync-on-rewrite no

no-appendfsync-on-rewrite这个参数单独说一下。设成yes时,重写期间主线程不做 fsync,把 fsync 交给操作系统,能减少重写期间的延迟抖动,但代价是重写期间丢失窗口变大。设成no则重写期间照常 fsync,可靠性优先。数据不能丢的场景建议no,能容忍重写期间多丢一点的可以设yes换平稳。

上线后的操作流程:

  1. 改配置,重启实例(或按版本支持情况动态开启 AOF)
  2. 用INFO persistence确认aof_enabled:1,观察aof_current_size开始增长
  3. 压测写入,观察aof_delayed_fsync是否增长、延迟是否有毛刺
  4. 手动执行一次BGREWRITEAOF,确认重写能正常完成、aof_last_bgrewrite_status:ok
  5. 模拟一次重启,确认能从 AOF 正常加载、数据完整
  6. 接入监控,把 5.5 节的指标配上告警

这套流程走一遍,AOF 的基本可靠性就有底了。剩下的就是根据实际运行数据微调重写阈值和刷盘策略。

最后分享一个我自己的习惯:每次调整 AOF 相关配置前,先在测试实例上用接近生产的数据量和写入压力跑一遍,重点看aof_delayed_fsync和重写耗时。AOF 的很多问题不是配置错了,而是配置和实际负载不匹配——同样的everysec,在 IO 充裕的盘上稳如老狗,在 IO 紧张的盘上就是延迟毛刺的来源。配置是死的,负载是活的,拿真实压力验证过再上,比照搬任何"最佳实践"都靠谱。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询