Redis 之所以快,是因为数据都存在内存里;但也正因为如此,一旦进程重启,所有数据都会消失。本文按照“为什么需要持久化 → AOF 日志怎么写 → AOF 重写与写时复制 → RDB 快照 → 混合持久化和 MP-AOF → 大 Key 对持久化的影响”的顺序,把 Redis 持久化这条主线彻底讲清楚。这一篇也是前文《Redis 高可用详解:主从复制、哨兵机制与故障转移》里预告过的持久化篇。


一、为什么 Redis 一定要做持久化

Redis 是一个基于内存的键值数据库,读写性能极高的代价,就是数据全部依赖于内存这一介质。可以设想一个非常真实的场景:某台 Redis 实例因为机器故障、进程被 kill 或运维误操作宕机了。因为 Redis 的数据都存在内存中,一旦重启,内存里的所有键值对会全部消失。

这对于线上系统来说是一个非常严重的问题:

  • 所有数据丢失,即使 Redis 进程被拉起来,实例里也什么都没有。
  • 重建缓存并不是一个廉价的动作,需要把冷数据从数据库慢慢回填到 Redis,这个过程通常比较漫长。
  • 在缓存重建期间,大量请求会直接打到数据库,很容易压垮后端,进而影响系统的高可用。

所以 Redis 必须提供一种“重启后仍然能恢复数据”的机制,这就是持久化。持久化的核心思想很简单:把内存中的数据以某种形式落到硬盘上,Redis 宕机重启之后,直接读取硬盘上的持久化文件恢复数据,从而尽可能保证服务的高可用。

围绕这个目标,Redis 提供了两种主要的持久化策略:

  • AOF(Append Only File):以“写命令日志”的方式记录每一条修改数据的命令。
  • RDB(Redis Database):以“内存快照”的方式,把某一时刻内存中的全部数据以二进制形式写入磁盘。

后面还会介绍 Redis 4.0 引入的混合持久化和 Redis 7.0 引入的 MP-AOF(Multi Part AOF),它们本质上是对上面两种基础方案的组合与优化。


二、AOF 持久化:写命令日志的完整链路

2.1 AOF 的基本原理与写入顺序

AOF 的思路非常直接:每来一条写命令,就把这条命令追加到 AOF 日志文件里。当 Redis 宕机重启时,只需要重新“执行”一遍 AOF 里的命令,就可以把内存状态恢复回来。这种方式的缺点也很明显:恢复过程是逐条命令 replay 的,比 RDB 直接加载二进制快照要慢很多。

一个容易被忽略但非常关键的点是:AOF 的写入顺序是先执行命令,再写日志。也就是说,Redis 会先读取客户端发来的命令,先在内存里执行完毕,然后再把这条命令追加到 AOF 日志文件。这与传统数据库中“先写日志再操作数据”的 WAL(Write Ahead Log)思路是相反的。
AOF执行顺序图

Redis 选择这种“后写日志”顺序,主要有两个好处:

  • 省掉了额外的语法检查开销:AOF 追加写日志的操作也是主线程完成的。因为命令已经在内存中执行过一遍了,如果这条命令有语法问题,在操作内存的时候就会报错,根本轮不到写日志。这样对于语法错误的命令,天然就不会被写入 AOF,避免污染日志。
  • 不会阻塞当前的写命令:命令先在内存里执行完,然后才去追加日志,这一步不会挡住这条命令本身的响应。

当然这种“后写日志”的顺序也有隐患:

  • 数据丢失风险:Redis 操作内存很快,但写硬盘相对慢很多。如果刚好在“内存操作完成、日志还没写入硬盘”的窗口里宕机,这条写命令就丢失了,等价于数据丢失。
  • 写盘慢会阻塞后续命令:因为追加写日志也是主线程做的事情,如果一次写盘特别慢或者出问题,会拖住下一条命令的执行。

2.2 一条写命令从内存到硬盘的全过程

要理解 AOF 的三种刷盘策略,需要先弄清楚一条写命令在 AOF 里到底经历了哪几步:
AOF写入到磁盘的过程

  1. 执行 Redis 命令,追加到 aof_buf:命令在内存里执行完成之后,会被追加到 server.aof_buf 缓冲区。这一步完全在 Linux 用户态完成。
  2. write() 系统调用写入内核缓冲区:Redis 通过 write() 系统调用,把 aof_buf 里的数据写入到 AOF 文件。这里非常关键——此时数据并没有真正落到硬盘上,而是被拷贝到了内核的 page cache 里,等待内核后续把数据刷回硬盘。
  3. 内核决定何时刷盘:page cache 里的数据什么时候真正写到磁盘上,是由内核根据自身策略决定的。

可以看到,写入硬盘的操作涉及从用户态切换到内核态。这种切换本身是有一定开销的,如果每一条写命令都要经历完整的“write + fsync”过程,就极有可能拖慢 Redis 主线程执行核心命令的效率,反而得不偿失。

2.3 AOF 的三种刷盘策略:Always、Everysec、No

fsync的流程

为了让用户在“可靠性”和“性能”之间自己做取舍,Redis 提供了三种 appendfsync 策略:

  • Always(总是):每次写操作命令执行完成后,同步将 AOF 日志数据写回硬盘(write + fsync)。这是最可靠的一档,但每条命令都要落盘,开销最大,极容易阻塞主线程。
  • Everysec(每秒):每次写命令执行完,主线程只调用 write() 把命令写入到 AOF 文件的内核缓冲区(page cache),然后由一个 BIO 后台线程每隔一秒异步调用一次 fsync(),把 page cache 的内容真正刷回硬盘。
  • No(不主动 fsync):Redis 不主动控制刷盘时机,write() 完成后就把控制权交给操作系统,由操作系统自己决定何时把 page cache 刷回硬盘。

这三种策略并不能同时兼顾“不阻塞主线程”和“绝不丢数据”,这两个目标本身就是此消彼长的关系,只能根据业务特性做取舍:

  • Always:可靠性最高,几乎不会丢数据,代价是每条写命令都要写盘,开销大,容易阻塞主线程,适合对数据可靠性要求极高、TPS 不那么极端的场景。
  • Everysec:性能与可靠性折中方案。绝大部分场景下性能接近 No,但仍然存在最长 1 秒的“空窗期”,也就是宕机时最多可能丢失 1 秒内的数据。这是 Redis 默认推荐的方式。
  • No:性能最高,数据丢失的可能性最大,而且不确定性也最高,因为完全依赖操作系统的刷盘节奏。只有在“允许丢数据、追求极致性能”的业务里才建议选它。

三、AOF 重写:让日志文件不至于无限膨胀

3.1 为什么必须做 AOF 重写

AOF 通过“追加写命令”来记录状态变化,随着 Redis 实例运行时间变长,AOF 文件会变得越来越大。更麻烦的是,AOF 里会有大量“对同一个 key 的重复写命令”,比如同一个计数器可能被更新了几十万次,但恢复的时候其实只需要执行最后一次 SET 就够了。

文件越大,带来的副作用越明显:

  • 磁盘占用越来越高。
  • 宕机恢复时需要 replay 的命令数量越多,恢复时间越长,对系统可用性影响越大。

因此,AOF 重写这件事就变得非常必要。

3.2 AOF 重写的基本思路:读内存重新生成命令

AOF 重写的思路是:当检测到 AOF 文件大小超过预设阈值时,直接读取当前 Redis 内存中的全部键值对数据,为每个键值对生成一条对应的写命令,重新组织成一个新的 AOF 文件。

这样做之后,重复命令、过期数据统统被过滤掉,最终得到的 AOF 文件相当于是“当前内存状态的最小命令集合”。

但显然,遍历整块内存、生成命令、写文件是一个非常消耗性能的操作,不可能让主进程去做,否则会严重拖累 Redis 的核心命令处理能力。因此 Redis 采用了 bgrewriteaof 机制:fork 出一个子进程,由子进程去做 AOF 重写,主进程仍然照常处理客户端请求

3.3 子进程如何面对“主进程还在写”的问题

一旦 fork 出子进程做重写,就必然会遇到一个棘手的问题:在重写过程中,主进程还在不停地接收写命令,怎么保证子进程写出来的新 AOF 文件跟最终内存状态一致?

如果每次主进程有写命令都跑去通知子进程“你要更新这个 key”,然后让子进程重新去改新文件里的对应条目,这个开销是完全无法接受的。Redis 采用了一种更精巧的做法:Copy-On-Write(写时复制,简称 COW)

3.4 Copy-On-Write 机制详解

理解 COW,需要先理解“页表”和“物理内存”的关系。页表记录的是虚拟内存到物理内存的映射关系。子进程是由主进程 fork() 出来的,此时它的页表是从主进程复制过来的,也就是说:主进程和子进程的虚拟内存指向的是同一片物理内存
fork过程
在 fork 时,操作系统还会把这些共享的物理页设置为只读。这样做的目的是:

  • fork 阶段并不真正复制物理内存,只复制页表,速度很快,尽量减少对主进程的阻塞。
  • 当主进程或子进程尝试修改某个共享的物理页时,会因为“只读”触发缺页异常。
  • 操作系统在异常处理中,把这块物理页复制一份到新的物理地址,把这个进程页表指向新地址,并把权限改回可读写。之后这个进程就在新的物理页上继续写。

这就是所谓的“写时复制”:只在真正发生写操作的时候,才复制物理内存。这样可以避免 fork 时因为一次性复制整块物理内存而长时间阻塞主进程,只有真正被修改的那部分内存才会被复制。

COW过程

当然它并不是没有代价:

  • 如果页表本身很大(内存占用很高的实例),fork 时复制页表这个动作本身就会带来相对明显的阻塞。
  • 触发 COW 时,需要执行一次物理内存复制,这段时间对主进程也是阻塞的,尤其是被修改的内存页比较大时更明显。

3.5 AOF 缓冲区 vs AOF 重写缓冲区

整体概览

AOF整体概览
主进程在重写期间收到的写命令,不能只写到旧 AOF,否则新 AOF 一旦生成出来就跟内存对不上;也不能只写到新 AOF,否则如果重写失败,就意味着这些命令彻底丢了。Redis 的做法是:同时写两份缓冲区

  • AOF 缓冲区(aof_buf):给旧的 AOF 文件用。主进程处理完写命令后,正常把命令追加到 aof_buf,最终按刷盘策略写回到当前正在使用的旧 AOF 文件。这保证了在新 AOF 还没生成完成之前,旧 AOF 依然是完整可用的。
  • AOF 重写缓冲区:专门给新的 AOF 文件用。主进程收到写命令时,同时也把这条命令记录到 AOF 重写缓冲区。这样即便新 AOF 文件在生成过程中失败,也不会导致这部分数据丢失;如果生成成功,这些命令还可以补写到新 AOF 里。

当子进程完成新 AOF 文件的编写之后,会向主进程发送一个信号(信号是进程间通信的一种方式,是异步的)。主进程收到信号后,会调用一个信号处理函数,主要做两件事:

  • 将 AOF 重写缓冲区中的所有内容追加到新 AOF 文件末尾,使新旧两个 AOF 文件所记录的数据库状态一致。
  • 对新 AOF 文件进行 rename,覆盖掉原来的 AOF 文件。

信号处理函数执行完之后,主进程继续像往常一样处理命令。整个 AOF 后台重写过程中,除了 COW 会对主进程造成阻塞,信号处理函数执行时也会对主进程造成阻塞,其他时候都不会阻塞主进程。

3.6 Redis 7.0 之后的 MP-AOF:Multi Part AOF

上面描述的重写机制是 Redis 7.0 之前的方案。7.0 开始,Redis 引入了 MP-AOF(Multi Part AOF),把原本一个 AOF 文件拆成了多个文件:

  • 清单文件 manifest:记录当前 AOF 由哪些文件组成。
  • 基础文件 base:使用 RDB 格式保存重写开始那一刻内存中的全量数据。
  • 若干个增量文件 incr:使用 AOF 格式记录重写开始之后新产生的写命令。

在 MP-AOF 下,重写流程也变得更清爽:

  • 子进程还是通过 fork 生成,读取当时的内存状态,但不再生成 AOF 文件,而是直接生成 RDB 格式的 base 文件
  • 此时如果主进程有新的写命令进来,仍然会走 COW(这一部分逻辑没有变),但这些新命令不再写入“AOF 重写缓冲区”,而是直接写入一个新的 incr 文件
  • 重写完成后,Redis 只需要原子地更新 manifest 文件,让它指向新的 base 和新的 incr,旧的 base 和旧的 incr 就可以安全删除。

相比旧方案,MP-AOF 的好处非常直接:

  • 不再需要父子进程之间通过管道同步重写期间的增量命令。
  • 不再需要在主进程中维护“AOF 重写缓冲区”,也不需要在重写末尾把缓冲区一次性合并进新文件。
  • 整体上减轻了主进程的内存和 CPU 压力,AOF 重写过程更加健壮。
  • 天然支持多文件形式,运维上也更加友好。

四、RDB 持久化:内存快照

4.1 RDB 与 AOF 的本质区别

如果说 AOF 是“日志”,那 RDB 就是“快照”。两者最本质的区别在于:

  • AOF 记录的是写命令:文本形式的命令流。
  • RDB 记录的是数据本身:某一时刻内存中所有键值对的二进制表示。

因为 RDB 是二进制格式的数据快照,加载 RDB 时不需要 replay 命令,而是直接把数据反序列化回内存,因此恢复速度比 AOF 快得多

但生成 RDB 的开销并不小:需要把内存中所有数据编码后写入磁盘,非常消耗性能。如果这一动作发生在主线程,那么在生成快照期间,主线程既要执行命令、又要写文件,会导致命令执行被阻塞较长时间。

4.2 save 与 bgsave

Redis 因此提供了两个命令:

  • save:由主线程直接执行快照生成。生成期间主线程被完全占用,Redis 无法处理其他命令,只有在数据量非常小、允许短暂阻塞的场景才建议使用。
  • bgsavefork 一个子进程来做快照生成。fork 完成后,主线程可以继续处理请求,子进程负责将内存中的数据写入 RDB 文件。fork 期间以及后续如果发生写操作,都会走 Copy-On-Write,思路和 AOF 重写完全一致。

4.3 默认触发策略:save 900 1 等配置

Redis 默认在配置文件里给出了如下几条触发条件:

1
2
3
save 900 1
save 300 10
save 60 10000

需要特别注意的一个细节是:别看选项名叫 save,实际上执行的是 bgsave 命令,也就是会 fork 子进程来生成 RDB 快照文件,而不是阻塞式的 save

这三行配置的含义分别是:

  • save 900 1:如果在 900 秒(15 分钟)内,数据库发生了至少 1 次修改,就触发 bgsave
  • save 300 10:如果在 300 秒(5 分钟)内,数据库发生了至少 10 次修改,就触发 bgsave
  • save 60 10000:如果在 60 秒(1 分钟)内,数据库发生了至少 10000 次修改,就触发 bgsave

三条规则任一命中即会触发。可以看到默认策略下的最短快照周期基本在几分钟量级——因为快照本身开销比较大,不适合过于频繁。

4.4 RDB 的两个隐患

正因为 RDB 是周期性生成的,它天然存在两个问题:

  • 数据丢失窗口较大:如果两次快照之间发生宕机,这段时间内的新写数据会全部丢失。以默认配置为例,最坏情况下可能丢失几分钟内的数据,可靠性明显比 AOF 差。
  • COW 带来的内存放大风险:bgsave 过程中,如果主进程又执行了写命令,同样会触发写时复制,进行物理内存复制。这些新的修改并不会被写进正在生成的这份 RDB 文件里,只能等下一次快照。如果在 bgsave 期间修改了大量共享内存,那么这些被修改的物理页都要被复制一份出来。极端情况下,如果被修改的量特别大,进程实际占用的物理内存可能接近翻倍,如果超过服务器物理内存上限,就可能出现操作系统 OOM 或宕机,Redis 自身也会跟着挂掉。

五、混合持久化:AOF 和 RDB 的结合

因为 RDB 单独使用时可靠性偏低(数据丢失窗口大),AOF 单独使用时恢复速度慢,从 Redis 4.0 开始,官方提供了一个把两者结合起来的方案,叫混合持久化。它可以通过下面这个配置开启:

1
aof-use-rdb-preamble yes

这种方式其实可以看作 MP-AOF 的前身。它的核心流程是:

  • 触发 AOF 重写时,子进程 fork 出来之后,先将与主线程共享的内存数据以 RDB 格式写入到 AOF 文件的前半部分
  • 在重写期间,主进程收到的新写命令通过 COW 落到 AOF 重写缓冲区。
  • 子进程完成 RDB 部分的写入后,重写缓冲区里的增量命令再以 AOF 格式写入到 AOF 文件的后半部分。
  • 写入完成后通知主进程用这个“RDB + AOF”格式的新文件替换旧的 AOF 文件。

这样带来的直接好处是:重启加载时,前半段是 RDB,加载速度非常快;后半段是增量的 AOF 命令,量比较小,逐条执行也不会太慢,整体恢复速度显著优于纯 AOF 方案。

不过如果使用的是 Redis 7.0 以上版本,建议优先使用 MP-AOF,理由前面已经讲过:

  • 解耦了主进程和子进程之间的增量同步逻辑。
  • 不再需要 AOF 重写缓冲区,主进程内存和 CPU 压力更小。
  • 支持多文件(manifest + base + incr),运维更友好。
  • 恢复更快,只需要原子地 rename manifest 文件即可完成新旧切换。

六、大 Key 对持久化的影响

前面讲的都是“正常大小”键值对下的持久化行为,但如果 Redis 实例中存在大 Key,那么持久化的每一个环节几乎都会被放大。

6.1 大 Key 对 AOF 日常写入的影响

从 AOF 日常同步的三种策略来看:

  • Always:如果修改了一个大 Key,那么对应的写命令本身就会非常长、数据量很大。这条命令要先写入 AOF 缓冲区,然后切换到内核态,把 aof_buf 里的数据 write 到 page cache,再 fsync 落盘。因为数据量很大,这一整套操作的耗时会显著变长,从而长时间阻塞主进程
  • Everysecfsync 由 BIO 后台线程异步执行,可以缓解一部分压力,但主线程仍然需要执行 write(),把大 Key 的数据从 aof_buf 复制到内核态 page cache。数据量大的时候,这次 write 本身就足以阻塞主进程。此外,如果上一次后台 fsync() 还没执行完,主线程的 write() 会被强制等待,此时大 Key 依然可能显著阻塞主线程。相比 Always,无论是 fsync 的频率还是每次同步的时间,都会好一些。
  • No:不需要主线程去等待 fsync(),只会拖慢 write 本身,因此在大 Key 场景下影响是三者中最小的,但可靠性相应也最差。

6.2 大 Key 对 AOF 重写与 RDB 快照的影响

对于 AOF 重写和 RDB 快照,大 Key 影响最严重的其实是 fork 后的 Copy-On-Write 阶段

  • fork 本身:主要跟内存整体规模(页表大小)相关,跟单个 Key 大不大直接关系不算特别大。
  • COW 阶段:如果在重写/快照期间对大 Key 做了写操作,那么这个大 Key 所在的物理页就会被复制。大 Key 的数据在物理内存里往往是连续的,即使只改了其中一小部分,整片物理内存都要被复制一份,非常拖时间和性能。
  • 对内存的冲击:多次大 Key 的 COW 会使内存使用量突增,如果操作了多个大 Key,甚至可能把 AOF 重写缓冲区打满,或让整个实例的物理内存占用接近翻倍。
  • 对普通 RDB:本质上和上面一样,都需要 fork 和 COW,COW 才是大 Key 影响 RDB 性能的根因

对于混合持久化:因为流程本质上是“前半段 RDB + 后半段 AOF”,所以在 COW 阶段处理 AOF 重写缓冲区、以及复制物理内存的开销都会因为大 Key 明显变大。

对于 MP-AOF:主进程新的写命令直接写入 incr 文件,也就是直接写磁盘,因此大 Key 会让每次写 incr 的耗时变久、开销变大;不过好处是不需要再走“AOF 重写缓冲区”这条内存路径,对内存本身的冲击相对小一些。

一句话总结:只要 Redis 里存在大 Key,无论选哪种持久化策略,都会在写盘或 COW 环节被放大成明显的性能问题,因此在持久化调优之前,先治理大 Key 往往才是最有性价比的动作。


七、总结

回到最开始的问题:Redis 为什么需要持久化?因为纯内存的架构决定了它一旦重启就会“失忆”,而重建缓存的过程又太慢,这直接冲击了系统的高可用。围绕这个目标,Redis 演化出了一整套持久化体系:

  • AOF:以写命令日志的方式记录变更,可靠性高但文件体积大、恢复慢;通过 Always/Everysec/No 三种刷盘策略在可靠性和性能之间做权衡;通过 bgrewriteaof + Copy-On-Write 完成 AOF 重写,避免文件无限增长。
  • RDB:以二进制快照的方式定期把内存状态整体落盘,恢复非常快,但存在较大的丢数据窗口,并且 bgsave 期间的 COW 有内存翻倍的风险。
  • 混合持久化(Redis 4.0):AOF 重写时先写一段 RDB,再追加 AOF,兼顾恢复速度和可靠性,是长期以来主流的推荐方案。
  • MP-AOF(Redis 7.0):进一步拆成 manifest + base + incr 的多文件结构,解耦父子进程之间的增量同步,去掉了 AOF 重写缓冲区,是当前最推荐的持久化方案。

一些落地建议:

  • 可靠性要求极高、TPS 不极端:选 AOF Always,或 MP-AOF + Always。
  • 绝大多数在线业务:Everysec 是默认也是最平衡的选择,配合 MP-AOF 或混合持久化使用体验最好。
  • 允许一定数据丢失、追求极致性能:可以考虑 No 或纯 RDB,但需要非常清楚业务能容忍多大的数据丢失窗口。
  • 无论选什么方案,都优先治理大 Key:大 Key 是持久化性能问题的头号放大器,无论是日常刷盘还是重写/快照,都会因为它被显著拖慢。

Redis 的持久化机制其实并不复杂,但每一个细节背后都是在“性能”“可靠性”“恢复速度”这三个维度之间做权衡。理解了这些取舍,再回过头去看 appendfsyncbgrewriteaofsave 这些配置,就会发现它们不再是一堆孤立的选项,而是一条完整的、可以按业务需求组合出来的持久化策略链路。