☰
Redis持久化模型详解:RDB、AOF与Fork机制及实践避坑指南
2026/10/8 4:02:59 网站建设 项目流程

1. 为什么会讲持久化模型

1.1 内存数据库的短板:重启即失忆

先从一个我实际遇到的问题说起。线上有个 Redis 实例,跑了快三个星期,内存里堆满了订单状态、限流计数和热点配置。某天凌晨物理机硬件报警,紧急重启之后,Redis 里一个 key 都没剩,业务方一脸懵。排查完发现原因很简单:这个实例没开持久化。Redis 的持久化模型,翻来覆去核心就三个词:RDB、AOF、Fork。

任何把 Redis 当“正经数据源”使用的人都绕不开这个问题:它是内存数据库,数据来自内存,进程退出、机器重启,内存被回收,数据立刻消失。我们要做的,是在进程活着的时候根据一套机制把内存里的数据落到磁盘上,下次启动再加载回来,这个过程就叫持久化。Redis 的持久化也就两套方案:RDB 和 AOF,外加一个支撑它俩能“后台运行”的 Fork。

这篇是“从零开始学习 Redis”系列第三篇,我先在开头把事情说清楚:RDB 是往磁盘写一份完整的二进制快照;AOF 是把每一条改写数据的命令追加到日志;Fork 则是底层支撑这两者的操作系统机制。搞懂三者的协作关系,你才能真正理解 Redis 崩溃恢复、主从复制、甚至集群扩容背后的一部分本质。

适合看这篇的人,我判断是两类。一类是把 Redis 当纯缓存用,还没搞清楚“为什么重启丢数据”的新手;另一类是在线上遇到过数据丢失、恢复慢、延迟突刺,想弄明白原因再去调参数的运维和开发。内容尽量从原理讲到配置,再补上我实际踩过的坑,尽量让你读完之后心里有底。

1.2 快照、日志和进程分身,先摆到一张桌上

给不熟悉的读者做一个横向对比,后面读起来不容易迷糊。

RDB,全称 Redis Database File,简单理解就是 Redis 在某个时间点给全部内存数据拍了一张“照片”,序列化成二进制文件 dump.rdb。恢复时把这张照片重新加载回内存就行。

AOF,全称 Append Only File,更像记账本。Redis 每执行一条写命令,就把这条命令按自己的协议格式追加到一个文件中。恢复时重放这个记账本,把命令一条条再执行一遍,数据就回来了。

Fork 是操作系统提供的进程复制调用,作用是让一个进程“分身”出一个几乎一模一样的子进程。Redis 做后台持久化和日志重写,都会靠 Fork 来分身——子进程慢慢写盘,父进程继续服务客户端。

下面这张表是我日常给人讲持久化时最常放在桌面上的对比,建议收藏:

维度RDBAOF
数据记录方式某个时刻的全量内存快照追加记录写命令日志
文件大小相对紧凑通常远超 RDB,定期重写才能控制
数据丢失窗口取决于触发间隔,可能较大everysec 策略最多丢 1 秒
恢复速度快慢,尤其文件大时
适合场景备份、批量恢复数据完整性要求更高的场景

Fork 不直接产生持久化文件,但它是 RDB 的 bgsave、AOF 的自动重写能“后台执行”的先决条件。理清这层关系,下面分三块拆开讲。

2. RDB——内存快照,简单粗暴却暗藏门道

2.1 触发 RDB 的三种方式,线上别随便敲 save

RDB 的生成有三种触发方式,我在生产环境里基本都碰过。

第一种是save命令,执行它是同步的。Redis 收到 save 之后会停下手头所有读写,立刻生成一份 RDB 文件,写完才继续响应。这种操作只在极少数“我想立刻备份一个当前档”的场合下使用。线上千万别随手敲,数据量大时它会直接堵住整个实例,客户端大面积超时可以非常酸爽。

第二种是bgsave(background save),后台保存。Redis 收到命令后通过 Fork 创建子进程,由子进程去生成 RDB 文件,主进程继续处理正常业务。线上自动触发和手动触发 RDB,基本都走这条路。

第三种是配置文件里的自动触发规则。下面这套是 Redis 默认配置里最经典的参数,很多环境到现在还是这套:

save 900 1 save 300 10 save 60 10000

这三行含义:900 秒内发生至少 1 次写操作,就 bgsave 一次;300 秒内发生至少 10 次写操作,就 bgsave 一次;60 秒内发生至少 10000 次写操作,就 bgsave 一次。满足任意一条,后台定时任务就会触发保存。

注意这里的“写操作”不是看命令条数,而是看 Redis 内部的 dirty 计数器。它按发生修改的 key 数量累计,没改数据的读命令不计数。这套规则背后的思路,是让数据丢失窗口和写入频率做一个平衡:写得多就勤保存,免得崩了丢太多数据;写得少就少存几次文件,省磁盘和 CPU。理解这个逻辑,你再去改 save 配置就知道自己在调什么了,而不是死背参数。

2.2 bgsave 的执行流程,临时文件和原子改名很关键

bgsave 完整流程,建议每个用 Redis 的人都记在脑子里,面试答得出、线上排障用得上。流程大概是:

  1. 主进程调用 fork() 创建子进程。
  2. 子进程把当前内存里的完整数据集,写入一个临时 RDB 文件。
  3. 子进程写完临时文件后,用 rename 原子替换掉旧的 dump.rdb 文件。
  4. 子进程返回结果,主进程记录保存时间。

为什么要写临时文件再改名?为了保证磁盘上的 RDB 文件始终是一份完整的文件。如果直接覆盖原文件,写盘中途断电或崩溃,老文件和新文件都残缺,那真就彻底没法恢复了。这个“临时文件 + 原子改名”的思路,后来我在处理其他中间件数据备份时也经常借鉴,非常实用。

可能有人会问:bgsave 期间父进程被改了的数据,子进程能看得到吗?答案是不能,也不需要。子进程生成的是“fork 瞬间”的内存快照。如果 fork 之后有 100 个 key 被改动,这 100 个 key 的最新值不会出现在本次 RDB 文件里,它们要么等下一次保存,要么在 AOF 里。这个语义一定要理解,否则你会以为 bgsave 之后的数据一定安全,其实在 RDB 生成的这段时间窗口里,数据依然可能丢。

2.3 RDB 文件内部结构:一个二进制文件的自我修养

很多人以为 RDB 文件就是个简单的 key-value 文本,其实它的结构是精心设计过的二进制协议。文件开头五个字符是魔数REDIS,紧跟着 RDB 版本号;再往下是若干辅助字段、真正的键值数据;文件末尾有结束标记,并且带一个 CRC64 的校验码,用来判断文件在写盘或搬运过程中有没有损坏。

Redis 加载 RDB 时,如果文件损坏,会直接拒绝启动并报错,不会拿半截数据硬撑。这也解释了为什么有时候你从云盘导出的 dump.rdb,拷贝到另一台机器上打不开,多半是传输过程中被截断,或者源头磁盘就已经写坏了。

RDB 文件还支持压缩,对应配置是rdbcompression,默认开启。重复度高的字符串值被压缩之后,磁盘文件往往比内存数据小不少。配置文件里还有个rdbchecksum,默认也是开启状态。日常维护数据备份,我会在导出 RDB 后特意对文件算一次 md5,和源文件对比,防止网络拷贝出问题。

2.4 RDB 的优点和痛点,一张表说清

优点方面,RDB 恢复速度极快,Redis 重启时直接加载一份二进制快照即可,不需要逐条执行命令,对大实例优势明显;文件紧凑,适合做冷备;还方便异地传输,拷贝一份就能快速拉起新实例。

痛点也很明显。首先数据丢失窗口大,如上面配置save 900 1,最坏情况可能丢 900 秒的写数据。其次,全量快照写盘期间,如果内存很大,fork 出来的子进程写磁盘要持续一段时间,期间 COW 机制可能导致内存增长,IO 压力大时也会间接影响主进程性能。最后,如果业务写入量特别大,RDB 触发频繁,每生成一次全量文件,磁盘都要经历一次不小的写放大。所以很多团队不会只依赖 RDB,而是选择 RDB 加 AOF 组合使用。

3. AOF——追加日志,把每次写操作当成“备忘录”

3.1 AOF 日志记录的是命令本身,而不是数据结果

AOF 和 RDB 的定位完全不一样。RDB 记录“此刻数据长什么样”,AOF 记录“谁改了数据、怎么改的”。Redis 收到一条写命令,比如set foo bar,它会先把这条命令按 RESP 协议格式追加到文件缓冲区,再根据配置决定什么时候真正刷到磁盘。命令用于恢复数据时逐条重放,因此恢复过程本质上是按时间轴重演。

看一段 AOF 文件里的原始内容你就有感觉了,RESP 格式大致长这样:

*3\r\n$3\r\nset\r\n$3\r\nfoo\r\n$3\r\nbar\r\n

*3表示这是一个包含 3 个参数的命令;$3表示接下来字符串长度是 3,后面依次跟着set、foo、bar。恢复时 Redis 读文件,按协议把命令一条条解析出来再执行一遍,数据就回来了,像看回放一样。

这种设计有一个额外好处:AOF 文件是文本协议,人可以直接查看,甚至可以做数据校验。有一次线上误执行了一条 flusall,我就是先从 AOF 里定位到那条危险命令,分析了它前后的写入顺序,再决定怎么恢复的。RDB 是二进制内容,做类似分析就费劲多了。

3.2 fsync 策略:always、everysec 和 no,到底丢多少数据

AOF 写盘策略有三个配置值:always、everysec、no。这三个词直接决定了崩溃时你会丢多少数据,也直接决定写性能。

always:每条写命令执行完后,立刻调用 fsync 把数据从内核缓冲区刷到磁盘。崩溃时最多丢失还没写完的最后一条命令,安全性拉满。代价也最大,高频写入场景下磁盘 fsync 会成为瓶颈,吞吐会明显下降。

everysec:每秒把累积的命令批量刷一次盘。理论上崩溃时最多丢失 1 秒内执行的写命令。对绝大多数业务来说这是性能和安全的平衡点,也是生产环境最常用的配置。

no:完全交给操作系统,由系统决定什么时候刷盘。数据会先写在内存缓冲区,由内核找机会落盘。性能最好,但一旦进程崩溃或机器断电,丢失多少完全取决于系统刷盘时机,很难预估,线上基本不建议单独使用。

可以直接收藏这个表:

策略丢失范围性能影响适用场景
always最多丢未刷盘的 1 条写命令明显强一致场景,能接受性能换安全
everysec最多丢 1 秒内写命令较小默认推荐,绝大多数业务
no不确定,取决于系统刷盘最好可接受大量丢失的缓存场景

3.3 AOF 为什么必须重写:同一个 key 被改一万次的后果

AOF 有一个绕不开的问题:文件不断膨胀。同一个 key 被 update 一万次,RDB 里只留下这个 key 的最终值,而 AOF 里会留下这一万条命令。恢复时要把这一万条命令全执行一遍,启动慢,文件也占地方。

解决办法是执行bgrewriteaof命令做重写。重写时同样通过 fork 创建子进程,子进程基于当前内存里的最新状态,生成一份最小化的 AOF 文件;主进程这期间收到的新写命令会先记录在内存缓冲区,等子进程写完后,Redis 再加把这些增量命令追加到新文件尾部,最后原子替换旧文件。整个重写过程用户无感。

自动重写的触发,由两个参数控制:

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

含义是:当 AOF 文件比上一次重写后的基准体积增长了 100%,并且文件已经超过 64MB,就触发自动重写。所以如果你发现某个实例的 AOF 文件异常增长,要重点看最近有没有发生过重写、写入速度是不是远超预期。

3.4 AOF 的优缺点摆上台面

AOF 最大优势是数据安全性高,崩溃丢失窗口更小;文件可读,便于排查和人工修复。但别盲目迷信它。AOF 文件通常比 RDB 大许多,恢复时需要按顺序 replay 命令,启动时间往往比 RDB 慢一个量级;如果重写不及时,文件膨胀到几个 GB 之后恢复太慢,也会拖垮业务。

所以线上常见做法,不是“只开 RDB”或“只开 AOF”,而是两者结合,同时开启混合持久化。这一点到第 5 章再说。

4. Fork——持久化背后的核心机制

4.1 Redis 为什么离不开 Fork

先问一个问题:为什么 RDB 后台保存和 AOF 重写,都要 fork 一个子进程去干活,而不是主进程直接写磁盘?原因很直接:磁盘写入速度跟内存吞吐差距太大,如果主进程自己写全量快照,写盘期间所有客户端请求都会被阻塞。

fork 是 Linux 创建进程的系统调用,调用之后,系统生成一个几乎一样的子进程。父进程和子进程共享同一份代码段和内存映射。Redis 的做法是:父进程继续响应客户端,子进程拿着当前的内存镜像慢慢把数据写进磁盘文件。整个过程对调用方来说像“多了一个自己”,既不会额外占用 CPU,也不需要把内存整体复制一遍,这是 fork 最大的价值。

没有 fork,bgsave 只能退化成同步 save;没有 fork,AOF 重写也做不到后台执行。所以,fork 是整个持久化体系里那个不出镜但绝对离不开的角色。

4.2 写时复制(COW)到底复制了什么

提到 fork 必然要说写时复制(Copy On Write,COW)。不少新手以为 fork 之后内存直接翻倍,其实这是误解。fork 刚完成时,父子进程共享同一批物理内存页,并没有复制真正的数据,只是复制了页表这种“索引关系”。真正开始复制,是某个进程去修改某个内存页的时候。

以 bgsave 为例,子进程正在读内存生成 RDB,主进程此时如果收到写命令,要改某个 key,那么这页内存就会被复制一份。主进程改自己的新副本,子进程继续读旧副本,保证子进程生成的文件是“fork 瞬间的数据快照”。这就是写时复制的精妙之处。

这个机制决定了一个重要结论:bgsave 期间的实际内存开销,取决于期间写入了多少数据,而不是实例总内存有多大。比如实例内存 8GB,bgsave 期间只改了 500MB 数据,那 COW 最多额外消耗几百 MB 量级的内存。我一般会盯监控里 bgsave 期间的 RSS 增长,看到它突然往上顶,就知道这个实例当前写入压力不小。

4.3 影响 fork 耗时的关键因素:页表、THP 和系统内存

fork 本身虽然不复制全部内存,但也不是零成本,因为它要复制进程的页表。内存越大,页表越大,fork 耗时越长。我实测过一个约 16GB 的 Redis 实例,内存接近满载时,linux 下 fork 一次通常要 30 到 80 毫秒,极端情况能到 150 毫秒以上。这块时间虽然不长,但对高 QPS 实例来说,已经能让一批请求超时了。

更坑的是透明大页(Transparent Huge Pages,THP)。THP 开启后,一个页面变成 2MB,哪怕只修改一个小字符串,也可能导致整个 2MB 页被复制,内存开销瞬间放大几倍。Redis 官方一直建议关闭 THP。常见做法是修改内核参数:

echo never > /sys/kernel/mm/transparent_hugepage/enabled

同样,系统内存压力也会显著拖慢 fork。fork 时系统要分配新的内核结构,如果机器本身 swap 频繁,fork 时间会非常难看,甚至触发 OOM Killer 误杀。所以生产环境里,Redis 内存预算和系统绝对内存之间一定要留出 COW 的余量,别把机器填到一丝不剩。否则一旦 bgsave,内存很容易爆。

4.4 怎么判断实例被 fork 拖累了

想知道 fork 花了多久,不用猜,直接看info stats里的latest_fork_usec字段,单位是微秒。这个值稳定在几千到几万微秒,说明还算健康;如果动不动几十万甚至上百万微秒,就要警惕了。

我排查 fork 延迟时的顺序一般是这四步:

  • 先用info stats和info persistence看latest_fork_usec、rdb_last_bgsave_status、aof_last_bgrewrite_status。
  • 再用redis-cli --latency看有没有周期性延迟尖刺,和 bgsave、bgrewriteaof 的触发时间对一下。
  • 同时看机器可用内存,排除 swap 和内存不足。
  • 最后才考虑是否需要调低 RDB 触发频率,或者把实例拆小。

这套方法我用过很多次,能定位到大部分 fork 相关故障来源。

5. 配置文件里的持久化选型,到底怎么定

5.1 关键配置项逐个解读

把 RDB、AOF 的原理搞明白之后,配置文件怎么填其实就清晰了。下面是我常用的一套生产配置(数值按场景调整),逐行做个注释:

# RDB 触发条件 save 900 1 save 300 10 save 60 10000 # RDB 写入失败时是否继续接受写入 stop-writes-on-bgsave-error yes # RDB 压缩与校验 rdbcompression yes rdbchecksum yes # AOF 开关 appendonly yes appendfilename "appendonly.aof" # AOF 写盘策略 appendfsync everysec # 自动重写阈值 auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb # 混合持久化 aof-use-rdb-preamble yes

这里重点解释两个容易被忽略的配置。stop-writes-on-bgsave-error默认是 yes,意思是后台保存出错时,比如磁盘满了,Redis 为了防止用户产生“数据还在”的错觉,会直接拒绝新的写请求,直到后台保存恢复成功。刚接触 Redis 的人可能觉得这个行为很怪,但它的本意是保护数据一致性,生产环境建议保持默认。

另一个是aof-use-rdb-preamble,开启后 AOF 文件开头先放一份 RDB 快照,后面再接增量写命令。恢复时先快速加载 RDB 部分,再重放很小一段增量命令,既享受 RDB 恢复快的优势,又保留 AOF 丢失窗口小的能力。Redis 4.0 之后混合持久化已经非常成熟,除非有特殊兼容需求,否则建议开启。

5.2 数据恢复流程和选型原则

先看 Redis 启动时的恢复逻辑:如果开了 AOF 且文件存在非空,Redis 优先使用 AOF 恢复;只有没开 AOF 或文件不存在时,才会加载 RDB。所以,两个都开且都有效时,AOF 是主角,RDB 更多承担备份和快速拷贝的角色。

再给不同场景一个选型参考:

  • 纯缓存场景,数据丢了回源就行:可以不开持久化,或者只在低峰手动 bgsave,用 RDB 就够了。
  • 缓存加一定数据容忍度,热点配置丢了一会儿没事:默认 RDB,可加开 AOF everysec 做双保险。
  • 数据比较重要,比如库存、订单状态、活动流水,重启丢几秒不能接受:必须 AOF everysec,甚至 always;同时定期生成 RDB 做异地冷备。
  • 主从复制已经搭好且从节点足够多:可以让从节点承担持久化任务,主节点专注读写。不过要留意从节点也会触发 bgsave,别让从节点背上不必要的 fork 负担。

我个人的习惯是:能开混合持久化就开混合持久化,然后通过监控盯 AOF 文件膨胀和 fork 时间。如果哪一天发现恢复时间不可控,再回到“每天凌晨 bgsave 一次 RDB + everysec 的 AOF”这个经典组合。持久化模型不折腾才是长久之道。

5.3 主从和集群模式下的持久化注意点

最后单独提醒一下主从架构里的持久化。主从复制时,从节点默认复制主节点的数据,从节点的持久化文件主要用来备灾。但如果你把从节点当热备来用,注意从节点同样会执行 bgsave 和 AOF 重写,也会产生 fork 和磁盘 IO。挂了不少只读流量,慢日志里就可能出现 fork 引起的顿挫。

集群模式下每个分片节点都是独立的 Redis 实例。做持久化参数变更,比如切换 AOF 策略、改 RDB 触发频率,请务必分批滚动执行,先拿一个副本节点验证,再逐步推全量,别一把梭哈所有节点。很多线上故障,都是因为“看着配置很简单,就全局一起改了”导致的。

6. 我在生产环境实测踩过的持久化坑

6.1 fork 耗时过长,引发高可用切换误判

之前有个业务,Redis 集群跑在物理机上,内存普遍接近 20GB。某次大促准备阶段,我们频繁调整参数,RDB 和 AOF 重写经常撞在一起。结果监控里出现了一批节点延迟尖刺,最长达到 300 毫秒。哨兵误判主节点失联,直接触发主从切换,差点把大促流量全打到从节点上。

复盘发现,问题根源就是内存太大,fork 页表复制时间不可控,加上重写触发太频繁,两个子进程同时存在,系统内存被 COW 吃得很紧。后来的处理方法是:把大实例拆成多个小实例,每个控制在 4GB 到 8GB,同时拉大 RDB 触发间隔,AOF 重写错峰安排在业务低峰。改完之后,latest_fork_usec从几十万微秒降到十万以内,延迟尖刺基本消失。

6.2 AOF 膨胀到几个 G,恢复时把自己等哭了

还有一次,某应用的 Redis 平时 AOF 没太关注,机器重启之后发现 appendonly.aof 已经涨到 7GB。启动时 Redis 一条条重放命令,整整用了十几分钟,期间业务完全不可用。为什么文件会膨胀成这样?核心就是写入量大,自动重写阈值不合理,一直没触发重写。

这种坑一旦踩了,短期救火只能手动执行bgrewriteaof,把日志压缩一遍。长期就得靠监控 AOF 文件大小,把自动重写阈值调得激进一些。我现在会让监控平台对aof_current_size超过 1GB 的实例直接告警,防止再有业务启动时被文件拖死。

6.3 混合持久化在老版本和第三方工具里的兼容性坑

混合持久化虽好,但在老版本 Redis 上要注意兼容性。有一次我们准备迁移一套 Redis 5 到新环境,新库默认开了混合持久化,旧的备份工具却不认这种格式,恢复脚本直接报错。后来才知道 Redis 4.0 之前根本没有这个特性,低版本的redis-check-aof等工具对新格式支持也不好。

所以做跨版本迁移,或者用第三方备份恢复工具时,一定要先确认两边对 RDB 格式、AOF 格式、混合格式都互相兼容再动手切。不然到恢复那一刻才发现工具不认,是真的会站在机房里汗流浃背。

6.4 我日常最常用的几个持久化监控命令

最后分享一组我日常必看的监控命令,排查持久化问题非常直接:

redis-cli info persistence redis-cli info stats | grep fork redis-cli --latency

info persistence会告诉你最近一次 RDB 保存是否成功、AOF 重写是否成功、上次保存时间和当前文件大小;info stats里重点看latest_fork_usec;--latency用来盯延迟抖动。三个命令配合,大部分持久化相关问题都能在发生之前看出苗头。

以我个人的经验,持久化选型和调优没有一劳永逸的参数组合,唯一不变的原则是“始终给自己留一条恢复的退路”。无论你选 RDB、AOF 还是混合模式,都应该定期做一次恢复演练,哪怕是半夜找低峰节点把备份载入一个临时实例,看看到底能不能启动、启动要多长时间。这个习惯救了我很多次,也省掉了好几次本来要熬夜抢救数据的通宵。

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

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

立即咨询