
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 的三种方式线上别随便敲 saveRDB 的生成有三种触发方式我在生产环境里基本都碰过。第一种是save命令执行它是同步的。Redis 收到 save 之后会停下手头所有读写立刻生成一份 RDB 文件写完才继续响应。这种操作只在极少数“我想立刻备份一个当前档”的场合下使用。线上千万别随手敲数据量大时它会直接堵住整个实例客户端大面积超时可以非常酸爽。第二种是bgsavebackground 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 的人都记在脑子里面试答得出、线上排障用得上。流程大概是主进程调用 fork() 创建子进程。子进程把当前内存里的完整数据集写入一个临时 RDB 文件。子进程写完临时文件后用 rename 原子替换掉旧的 dump.rdb 文件。子进程返回结果主进程记录保存时间。为什么要写临时文件再改名为了保证磁盘上的 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 最大的价值。没有 forkbgsave 只能退化成同步 save没有 forkAOF 重写也做不到后台执行。所以fork 是整个持久化体系里那个不出镜但绝对离不开的角色。4.2 写时复制COW到底复制了什么提到 fork 必然要说写时复制Copy On WriteCOW。不少新手以为 fork 之后内存直接翻倍其实这是误解。fork 刚完成时父子进程共享同一批物理内存页并没有复制真正的数据只是复制了页表这种“索引关系”。真正开始复制是某个进程去修改某个内存页的时候。以 bgsave 为例子进程正在读内存生成 RDB主进程此时如果收到写命令要改某个 key那么这页内存就会被复制一份。主进程改自己的新副本子进程继续读旧副本保证子进程生成的文件是“fork 瞬间的数据快照”。这就是写时复制的精妙之处。这个机制决定了一个重要结论bgsave 期间的实际内存开销取决于期间写入了多少数据而不是实例总内存有多大。比如实例内存 8GBbgsave 期间只改了 500MB 数据那 COW 最多额外消耗几百 MB 量级的内存。我一般会盯监控里 bgsave 期间的 RSS 增长看到它突然往上顶就知道这个实例当前写入压力不小。4.3 影响 fork 耗时的关键因素页表、THP 和系统内存fork 本身虽然不复制全部内存但也不是零成本因为它要复制进程的页表。内存越大页表越大fork 耗时越长。我实测过一个约 16GB 的 Redis 实例内存接近满载时linux 下 fork 一次通常要 30 到 80 毫秒极端情况能到 150 毫秒以上。这块时间虽然不长但对高 QPS 实例来说已经能让一批请求超时了。更坑的是透明大页Transparent Huge PagesTHP。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 --latencyinfo persistence会告诉你最近一次 RDB 保存是否成功、AOF 重写是否成功、上次保存时间和当前文件大小info stats里重点看latest_fork_usec--latency用来盯延迟抖动。三个命令配合大部分持久化相关问题都能在发生之前看出苗头。以我个人的经验持久化选型和调优没有一劳永逸的参数组合唯一不变的原则是“始终给自己留一条恢复的退路”。无论你选 RDB、AOF 还是混合模式都应该定期做一次恢复演练哪怕是半夜找低峰节点把备份载入一个临时实例看看到底能不能启动、启动要多长时间。这个习惯救了我很多次也省掉了好几次本来要熬夜抢救数据的通宵。