聊到 Redis,很多人第一反应就是快,纯内存操作、单线程模型、延迟能压到亚毫秒级,这套印象没什么问题。但当你在生产环境真正跑起来,就会发现“快”只是它的 A 面,B 面是——只要进程一退、服务器一重启,内存里那几 GB 甚至几十 GB 的数据如果没有特殊机制保护,转瞬就归零。Redis 持久化就是为了解决这个问题而存在的,它保证你在进程崩溃、机器断电、甚至整机报废之后,还能把数据从磁盘捞回来。这篇不打算从零普及 Redis 是什么,而是聚焦“持久化的实现”,把 RDB 快照和 AOF 日志这两条技术路线彻底讲透,包括它们各自的底层原理、触发机制、优缺点对比、生产配置方案,以及我在实际运维中踩过的坑和总结的排查方法。如果你正在负责 Redis 的部署和日常维护,或者想搞清楚“为什么我的 Redis 重启后数据少了”“AOF 文件为什么越来越大”这类问题,这篇文章会提供一个相对完整的参考。
1. 内容整体设计与思路拆解
1.1 为什么 Redis 需要两套持久化方案
很多人第一次接触 Redis 持久化时会觉得奇怪:不就是把内存数据存到磁盘嘛,为什么搞出 RDB 和 AOF 两套机制,还经常让人困惑该用哪个?其实这两套东西解决的问题是不同的,放在一起设计也是有历史脉络的。
RDB(Redis Database)是最早出现的持久化方式,它的本质是“全量快照”。所谓快照,就是每过一段时间,把当前内存里的全部数据整体序列化,压缩后写进一个二进制文件。这个思路很直觉,就像你给一个房间拍了一张全景照片,房间里所有东西的位置和状态都定格在按下快门那一瞬间。RDB 的优点是很明显的:文件紧凑,恢复速度极快,直接加载进内存就行,特别适合做备份和灾难恢复。但它的致命缺陷同样来自“快照”本身——你不可能每秒钟都拍一张全景照片,那样磁盘和 CPU 都吃不消,于是两次快照之间的数据更新就面临丢失风险。默认配置是 900 秒内只要有 1 次写入就触发,也就是说极端情况下可能丢 15 分钟的数据,这对很多业务来说是没法接受的。
AOF(Append Only File)就是为了弥补这个短板而生的。它的思路完全是另一个方向:不拍照,改为记账。每一条写命令执行完之后,按照 Redis 协议格式追加到文件末尾,就像一本流水账,把每一次操作都原原本本记录下来。重启的时候,只要从头到尾把账本“重放”一遍,就能恢复出完整的数据状态。AOF 的优势在于可控的丢失窗口——你可以选择每次写入都立刻刷盘,这样最多丢一条命令;也可以选择每秒刷一次,最多丢一秒的数据。但代价也很直接:文件会持续膨胀,而且恢复时需要逐条执行命令,比加载 RDB 快照要慢不少。
所以你看,RDB 和 AOF 本质上是一对互补方案,一个追求“恢复快、文件小”,一个追求“丢得少、够精细”。并不是哪一个绝对优于另一个,而是要根据业务场景取舍。
1.2 两套方案的底层设计哲学
我后来深入看源码和官方文档的时候,越来越觉得 Redis 持久化的设计很像现实生活中的备份逻辑:你既需要定期的“全量备份”存档,以防万一,又需要持续的“增量日志”兜底,保证丢失数据的时间窗口最小。全量备份耗时长、频率低,但恢复起来一劳永逸;增量日志同步频繁、开销小,但时间长了账本会越记越厚,需要定期整理。Redis 的 AOF 重写机制就是“整理账本”这个过程。
从设计哲学上说,RDB 选择的是“牺牲一部分写入频率,换取极致的恢复速度和文件紧凑度”;AOF 则选择“牺牲一部分磁盘写入和恢复性能,换取更小的数据丢失窗口”。理解了这层取舍,再看 Redis 支持的混合持久化方案,就非常顺理成章——既然全量快照恢复快,增量日志丢数据少,那我干脆把两者结合:AOF 文件开头放一个 RDB 格式的全量快照,快照之后追加这段时间产生的新增命令。加载时先秒载快照,再重放少量日志,既快又不丢数据。
这个设计并不是凭空想出来的,而是从实际运维痛点上一步一步演化出来的。早年间很多人为了恢复速度只用 RDB,结果丢了十几分钟的数据,被业务方找上门;后来切换成纯 AOF,数据是不丢了,但启动加载动辄几分钟,大实例上用户又受不了。混合持久化正是对这种两难处境的回应,也是 Redis 4.0 以后生产环境最常见的配置方式。
2. 核心细节解析与实操要点
2.1 RDB 快照的触发机制与底层实现
RDB 的触发条件远远不止配置里那几个 save 规则,这点很多人容易忽略。在 Redis 内部,RDB 文件的生成主要有四条路径:
第一条就是配置规则自动触发。save 900 1表示 900 秒内至少有 1 次写操作就生成快照,类似的还有save 300 10、save 60 10000,三个条件之间是或的关系,满足任何一个就执行。第二条是手动执行BGSAVE命令,这也是我日常巡检时最常用的主动备份手段。第三条是在 Redis 正常关闭时,shutdown 命令会把内存里的数据自动保存成 RDB 文件。第四条比较隐蔽,发生在主从复制场景中——从节点发起全量同步时,主节点会生成一份 RDB 文件发送给从节点,也会触发一次快照。
理解了触发路径,再看底层实现就很有意思。RDB 是通过fork()系统调用创建一个子进程来生成快照的,这个机制叫 copy-on-write,写时复制。我把这个机制拆开讲:
fork 的那一瞬间,子进程和父进程共享同一份物理内存页,不复制数据,所以 fork 本身非常快。之后父进程继续正常处理客户端请求,该写写、该改改;子进程则把当前看到的内存数据往磁盘上写。问题来了:如果父进程在子进程写盘期间修改了某个内存页,怎么办?操作系统的做法是,把这个被修改的内存页复制一份,父进程在自己那份副本上继续改,子进程看到的依然是 fork 时刻的旧页面。所以子进程生成出来的 RDB 文件,严格对应的是 fork 那一刻的数据快照,而不是写盘过程中不断变化的数据。这就是为什么即使你在凌晨 2 点触发 bgsave,文件内容依然是“那一刻”的完整状态。
这里有一个生产环境必须注意的关键点:虽然 fork 很快,但如果 Redis 实例内存特别大,fork 本身也可能造成毫秒级甚至亚秒级的停顿。更麻烦的是,如果父进程在此期间修改的页面很多,写时复制带来的额外内存开销也会很可观。我见过一个 20GB 内存的实例,在写入高峰期 fork 之后,内存峰值额外涨了将近 4GB,直接把容器给打挂了。后面在 4.2 节我会专门讲怎么规避这个问题。
RDB 相关配置里有几个参数我建议你重视起来。rdbcompression yes默认开启压缩,生成的文件会小很多;rdbchecksum yes默认开启校验,加载时如果数据损坏会直接报错,安全性高。还有stop-writes-on-bgsave-error yes,翻译过来就是“如果 bgsave 失败,就拒绝写入”。这个参数表面上很“暴力”,实际上是有意为之——既然连备份都失败了,你还继续写入,那数据就越来越脆弱。但在磁盘空间不足等场景下,这个默认值会导致线上写入瞬间全部报错,所以如果你能保证业务能容忍风险,可以考虑把它设成 no。
2.2 AOF 日志的写入链路与三种刷盘策略
AOF 的实现思路我前面说了是“记账”,但具体记账的细节值得展开。当appendonly yes开启之后,每一条写命令在执行成功后,都会以 Redis 通信协议的文本格式被追加到一个内存缓冲区aof_buf中。然后,根据你配置的appendfsync策略,决定什么时候把这个缓冲区的内容真正写入(或者说同步到)磁盘文件。
这里要理解一个操作系统层面的关键概念。程序调用write()把数据写入文件,通常是先写到内核的 page cache 里,并没有真正落到磁盘。要真正落盘,必须调用fsync()强制刷新。fsync是有代价的操作,它涉及磁盘寻道和写入,在高频写入场景下可能成为性能瓶颈。所以 Redis 把决策权交给用户,用appendfsync参数来控制数据持久化的彻底程度。
三种策略差异很大,我在实际选型时是这样权衡的:
appendfsync always:每执行一条写命令,就调用一次 fsync 强制落盘。这个策略的安全性最高,最多只会丢失正在写入的那一条命令,但代价是吞吐量明显下降,因为磁盘 fsync 的速度远低于内存处理的速度。在我的压测环境里,同样的写负载下 always 大概只能跑到 everysec 的 40%~60% 性能,SSD 上损耗小一点,机械硬盘上会非常惨烈。
appendfsync everysec:这是 Redis 默认的策略,也是绝大多数生产环境的选择。它的做法是每秒调用一次 fsync,把这一秒内积累的 aof_buf 数据统一刷盘。由于 fsync 是后台异步执行的,主线程不会被直接阻塞,所以对写入性能的影响小很多。代价是极端情况下(比如刚好在两次 fsync 之间机器断电)可能丢失最近一秒的写入。
appendfsync no:完全交给操作系统决定什么时候刷盘。这个策略性能最好,但不确定性也最大,如果操作系统还没来得及把 page cache 写回磁盘就断电,丢失的数据可能就是几十秒甚至更久之前的内容。我基本只在临时测试环境才会用 no,生产环境不建议碰它。
这里还有一个细节值得注意。官方文档里明确强调,AOF 是“写后日志”——命令先执行,再记录。这和很多数据库的 WAL(write-ahead log,预写日志)恰恰相反。MySQL 的 binlog 里,“先写日志再改数据”是为了保证事务的持久性和回滚能力;而 Redis AOF 选择“先执行再记录”,是出于性能和简便性的考虑——如果命令本身执行失败(比如类型错误、内存不足),Redis 就不会记录这条命令,从而避免把无效操作写进日志。同时,因为先执行再写日志,日志里记录的命令不会影响正常命令的执行流程,主线程不用等磁盘写完才继续服务。
2.3 AOF 重写:解决文件无限膨胀的关键机制
AOF 的问题是“账本越记越厚”。比如一个计数器字段,如果在一段时间内被 INCR 了一万次,AOF 文件里就会存一万条 INCR 命令;但实际上恢复时只需要一条SET counter 10000就够了。如果不做任何处理,AOF 文件最终会膨胀到难以想象的大小,恢复时间也越来越长。
AOF 重写(rewrite)就是用来解决这个问题的。它的核心思想不是去修改旧的 AOF 文件,而是基于当前内存中的数据,生成一份全新的、最小的 AOF 文件。具体过程是:Redis fork 出一个子进程,子进程遍历整个内存数据库,把每个 key 的最终状态转成最精简的写命令序列,写入一个新的临时 AOF 文件。与此同时,父进程继续处理正常的写请求,把这些新命令同时缓冲到一个aof_rewrite_buf里。等子进程写完新文件,父进程再把缓冲期间积累的新命令追加到临时文件尾部,最后用rename原子替换掉旧文件。整个过程数据不丢,新文件又是完全精简的。
触发重写的方式有两种。手动执行BGREWRITEAOF命令可以立即触发,我通常在调整 key 结构或者发现 AOF 文件体积异常时用。自动触发则依赖两个配置参数配合:auto-aof-rewrite-percentage和auto-aof-rewrite-min-size。前者控制增长率,默认 100,表示 AOF 文件比上次重写之后增长了一倍就触发;后者控制绝对阈值,默认 64mb,意思是就算增长率到了 100%,如果当前文件小于 64MB 也不会触发。这两个条件必须同时满足才会自动重写。理解了这个机制,你就明白为什么刚启动的 Redis 不会立刻触发重写了——文件还没到 64MB 呢。
重写是一个比较重的操作,因为需要 fork 子进程并遍历全量数据。建议你在业务低峰期手动执行,或者把自动触发的阈值调到一个相对保守的值,避免在写入高峰期突然触发导致 CPU 和磁盘 IO 飙高。
2.4 混合持久化:怎么用一份 AOF 文件同时吃到两种红利
Redis 4.0 之后引入了混合持久化,配置项是aof-use-rdb-preamble yes。开启之后,AOF 文件的结构会发生很有意思的变化:文件开头不再是一堆纯命令日志,而是先嵌入一份 RDB 格式的二进制快照,快照之后再追加重写之后产生的增量命令。
我举个例子来解释它的价值。假设某个实例内存里有 200 万个 key,纯 AOF 模式下重写出来的文件可能有几百 MB 的纯文本命令,启动加载时 Redis 要逐条解析、执行,花掉好几分钟。开启混合持久化后,重写生成的文件头部是一份压缩的 RDB 快照,加载时 Redis 先像加载 RDB 一样快速把 200 万个 key 装进内存,然后只需要重放快照之后的少量增量命令,整个启动时间可能从几分钟缩短到几十秒。这也是为什么现在新版本里官方强烈推荐保持这个默认配置。
但需要提醒一点,混合持久化对兼容性有要求。老版本 Redis(4.0 之前)是无法识别这个混合格式的,所以如果你有跨版本迁移或者回滚的计划,一定要提前确认目标版本的 AOF 加载能力。我在 4.4 节会专门展开这个坑。
3. 实操过程与核心环节实现
3.1 一份可以直接抄的生产配置清单
光讲原理不落地等于白讲。这里我给出一份我自己常用的 Redis 持久化配置模板,注释里写清楚了每个参数的含义和选型理由。你可以直接在此基础上调整。
# 快照相关配置 save 900 1 save 300 10 save 60 10000 dbfilename dump.rdb dir /data/redis rdbcompression yes rdbchecksum yes stop-writes-on-bgsave-error no # AOF 相关配置 appendonly yes appendfilename "appendonly.aof" appendfsync everysec no-appendfsync-on-rewrite no auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb aof-use-rdb-preamble yes逐条说几个决定性的选择。save 900 1、save 300 10、save 60 10000这一组是默认的三级策略,我保留了它们,因为它能比较好地平衡快照频繁度和数据丢失窗口。如果你对数据丢失非常敏感,可以把最后一条改成save 30 1000,缩短触发间隔。stop-writes-on-bgsave-error我特意改成 no,是因为我踩过一次线上事故——磁盘满了以后 Redis 直接拒绝写入,业务方看到的全部是写入报错,很长时间都没排查出来是持久化问题。改成 no 之后至少能保证服务不中断,但风险就是你明知道备份已经失败,数据安全性在下降,所以运维上必须要有相应的监控告警补位。
appendfsync everysec是默认值,也是安全性和性能之间的平衡点。如果你的业务对数据丢失是零容忍,就改成 always;如果你的写入量极大且能接受十几秒的丢失窗口,才考虑 no,但我仍然不建议。no-appendfsync-on-rewrite默认是 no,意思是重写期间仍然正常执行 fsync,数据更安全。而aof-use-rdb-preamble yes开启混合持久化,这个在 4.0 以上版本已经是官方推荐做法,建议保持开启。
3.2 日常运维必须掌握的持久化命令与指标
除了配置文件,命令行里的几个命令和INFO Persistence输出是排查问题时最重要的抓手。
先看几个常用命令。BGSAVE是后台生成 RDB 快照,命令立即返回,真正的工作在子进程里完成,适合生产环境手动备份;SAVE是同步生成快照,期间 Redis 会完全阻塞,只适合在停机维护时使用,我看过有人误在生产环境执行SAVE,直接导致线上 10 秒不可用,教训深刻。LASTSAVE会返回最近一次成功生成 RDB 的时间戳,可以配合监控判断快照是否正常。BGREWRITEAOF是手动触发 AOF 重写,同样在后台执行。
然后是INFO Persistence的输出,这里面的字段信息量很大。拿rdb_last_bgsave_status来说,它显示的是最近一次 bgsave 是否成功,如果值是err,基本可以断定刚才有一轮快照失败了。rdb_last_cow_size表示上次 bgsave 期间 copy-on-write 额外分配了多少内存,这个数字可以直接告诉你那次 fork 的内存开销有多大。AOF 方面,aof_last_write_status用来确认最近一次 AOF 写入是否成功,aof_current_size和aof_base_size分别表示当前文件大小和上次重写后的大小。这些字段在监控大盘上是很重要的持久化健康指标,建议纳入告警。
3.3 数据恢复与备份迁移的完整流程
恢复这件事,很多初学者的认知是“把 dump.rdb 放到目录下启动就行”,但对线上环境来说,远没有这么简单。我按场景整理了一套相对可靠的流程。
第一种是 AOF 开启的情况下恢复。只要appendonly yes,Redis 启动时会优先加载 AOF 文件,而不是 RDB。原因很简单:AOF 里的数据比 RDB 更完整,加载它更符合数据恢复的预期。你只需要确保appendfilename指定的文件存在且没有损坏。如果 AOF 文件损坏导致启动失败,Redis 会拒绝启动,这时要用redis-check-aof工具修复,后面 4.3 节详述。
第二种是只有 RDB 的恢复场景。把 dump.rdb 放到dir指定的目录下,确保dbfilename文件名一致,然后启动。加载过程中 Redis 会校验文件头尾,如果rdbchecksum开启,还会检查整个文件的校验和。损坏的 RDB 文件会导致加载失败,此时 Redis 会记录日志并拒绝以空数据启动,这是保护机制——它宁愿不启动,也不让你带着空库上线。RDB 文件的修复工具是redis-check-rdb,但说实话,RDB 文件损坏时能修复的概率比 AOF 低很多,所以我更建议把 RDB 当作一份“可重新生成的备份”,而不是唯一的救命稻草。
第三类是跨机器迁移。最稳妥的方式不是直接拷贝 Redis 数据目录,因为你不能保证拷贝过程中源 Redis 没有写入。我通常的做法是:在源实例上执行BGSAVE,然后把生成的 RDB 文件传到新机器,再配好从库关系,让新实例先从 RDB 全量同步一次,之后通过增量复制追平数据。这样可以做到数据几乎无损,同时不影响源实例的正常服务。如果是小数据量,也可以直接用redis-cli的--pipe模式把数据全部导出再导入,但大数据量时远不如迁移 RDB 文件高效。
4. 常见问题与排查技巧实录
4.1 不同配置下的数据丢失窗口
持久化选型第一步,不是看性能,而是先问自己一个问题:业务能容忍丢多少数据?把这个想清楚,配置自然就出来了。我按常见的几种配置总结了丢失窗口的对应关系:
| 配置组合 | 极端丢失窗口 | 适用场景 |
|---|---|---|
| 仅 RDB,默认 save 规则 | 数分钟到十几分钟 | 可容忍较大丢失的缓存场景 |
| AOF + appendfsync everysec | 1 秒左右 | 大多数业务系统默认选择 |
| AOF + appendfsync always | 最多 1 条命令 | 交易、支付等强一致核心链路 |
| 混合持久化 + everysec | 1 秒 + RDB 加载时间 | 生产环境推荐,兼顾恢复速度 |
表格里的“极端丢失窗口”指的是进程崩溃或断电这类最坏情况。为什么是“极端”?因为正常情况下 Redis 退出会触发一次优雅关闭,把数据完整保存,基本不丢;真正丢数据都发生在异常断电、进程被 kill -9 这类来不及保存的场景。所以你可以把“极端丢失窗口”理解为兜底保险的级别。
4.2 fork 卡顿与内存暴涨的真实案例
这是我在生产环境遇到过的非常典型的性能事故。当时有一个 Redis 实例内存大约 18GB,写入量比较高。某一周连续出现几次访问毛刺,通过监控看到那些时刻 Redis 的延迟从 1ms 飙到 200ms 以上,而且持续了将近 1 秒。一开始怀疑是慢查询,排查后确认没有慢命令,再回头看进程状态,发现那几次毛刺时刻对应的正是自动 bgsave 触发的时间点。
问题出在 fork 上。18GB 的内存页面非常多,fork 子进程时要创建大量页表项,虽然理论上是复制页表不复制数据,但这本身依然是一个重量级操作。更要命的是,fork 完成后,父进程在高峰期继续大量写入,触发了写时复制机制,大量内存页被复制,内存瞬间上升了几个 GB,如果宿主机的 cgroup 内存限制比较紧,极端情况下可能直接 OOM。
这里最重要的调优方向是减少 fork 的成本和连带放大效应。具体来说有三件事:第一,用CONFIG SET把 auto-aof-rewrite 的触发阈值调大,或者把 bgsave 的触发频率放低,避免在写入高峰期频繁 fork。第二,在操作系统层面关闭透明大页(THP),因为默认的 THP 会在内存分配时随机产生延迟,放大了 fork 写时复制的抖动,这个可以对 Redis 实例所在机器执行相关配置关闭。第三,给 Redis 实例的宿主机预留足够的内存余量,或者直接用更小规格的实例做分片,把单实例内存控制在 10GB 以内,这是最治本的方案。
4.3 AOF 文件损坏时的修复流程
AOF 文件损坏的常见症状是 Redis 启动失败,日志里出现类似“Bad file format reading the append only file”的报错。这个过程背后有一个安全机制:Redis 在加载 AOF 文件时如果发现内容不符合协议格式,不会跳过错误继续加载,而是直接终止,因为如果强行加载可能得到一份错误的数据。
修复的手段是redis-check-aof --fix。用法也很简单,指定文件路径执行修复命令,工具会扫描整个 AOF 文件,找到截断或异常的指令并把它们剔除,生成一份可加载的新文件。但这里一定要理解:--fix修复的本质是“丢弃坏数据”,所以被剔除的那部分命令对应的数据会永久丢失。恢复成功之后,建议第一时间把这份数据备份,并且检查丢失的数据是否可以通过其他途径找回(比如上游数据库重放)。
实际操作中有个顺序很重要:修复前一定要先复制一份原始 AOF 文件的备份,而不是直接对原文件操作。因为修复工具把文件改坏的情况虽然少见,但多留一份原始文件总没有坏处。另外,如果 AOF 文件特别大,修复过程可能比较慢,最好在业务低峰期做,修复完成后立刻重启验证数据完整性。
4.4 混合持久化的兼容性边界
我接触过不少需要降级 Redis 版本的场景,比如新版本有 bug 需要回退到稳定老版本,这时候混合持久化的坑就暴露出来了。
aof-use-rdb-preamble yes开启状态生成的 AOF 文件,头部分是 RDB 格式的二进制数据。老版本 Redis(4.0 之前)根本不认识这种格式,加载时直接报错。如果你必须降级,有两个选择:在降级前预先执行一次BGREWRITEAOF,等重写完成后把配置里的aof-use-rdb-preamble改成 no,手动触发一次重写,生成纯 AOF 格式的文件,然后再降级。或者简单粗暴一点,降级时把 AOF 文件删掉,用 RDB 文件重建数据——但这意味着 AOF 里那些比 RDB 更新鲜的数据会丢失,一定要先评估丢失量能不能接受。
4.5 持久化与主从复制、集群部署的配合经验
最后聊一个经常被忽略的实践点。在搭建主从架构时,有人为了省磁盘和 IO,把主节点的持久化完全关闭,觉得“从节点反正有数据,主节点挂了能顶上”。这个思路在某些场景下可行,但风险非常高。主从复制的全量同步阶段,主节点需要生成 RDB 文件发送给从节点,它根本绕不开 RDB 这个底层机制。如果主节点同时关闭了持久化,一旦主节点异常重启,内存里已经是空的,此时从节点的数据就成了唯一指望。更糟的是,如果恰好从节点也要做全量同步,主节点连生成 RDB 的数据都没有,整个复制链路直接就断了。
所以我的推荐配置是:主节点正常开启持久化,从节点也必须开启持久化。不仅是为了容灾,也是为了让从节点可以作为数据备份的另一个副本。在集群环境里,每个分片至少要有两个副本承载持久化,这样即使单节点发生物理故障,集群也能安全恢复。
我个人在实际操作里的体会是,Redis 持久化的配置没有一劳永逸的“银弹”,它的每一步选择都是在丢失窗口、恢复速度、写入性能之间做权衡。搞清楚 RDB 和 AOF 各自的实现原理之后,很多看似玄学的故障(比如重启慢、fork 卡顿、AOF 膨胀)都会变得有迹可循,排查起来也更有方向感。最后再分享一个小习惯:每次调整持久化配置之前,先备份旧配置和当前数据,改完之后用一个小流量实例压一遍启动流程,确认加载时间和数据完整性都没问题再全量推广。这个习惯帮我避免过好几次线上事故,也推荐给你试试。