在Java后端项目里,Redis几乎成了标配的中间件,但绝大多数团队对它的定位是模糊的——嘴上说着"这就是个缓存",操作上却把用户会话、验证码、限流计数、分布式锁记录一股脑往里面塞。等到Redis进程异常退出或者宿主机宕机,恢复之后发现数据少了一大截,才开始重新讨论持久化方案。这篇内容把Redis持久化和宕机恢复机制完整拆一遍,覆盖RDB快照、AOF追加日志、混合持久化,以及从崩溃到恢复的完整排查链路,希望能帮正在设计缓存层、或者被"数据到底会不会丢"折磨的Java开发少走几步弯路。无论你是只写过get/set的新手,还是已经在维护集群的架构师,都能在对应章节找到有价值的东西。
1. 先问自己:Redis在系统里是缓存,还是准存储?
1.1 三种定位决定了你要不要开持久化
我见过不少架构图,Redis那一层永远标注着"缓存层",但实际落进去的数据早就超出了缓存的范畴。定位不同,持久化的必要性完全不一样,我习惯把数据分成三类:
- 可重算的纯缓存数据:商品详情页的热点数据、搜索结果缓存、临时渲染数据。这类数据丢了可以从数据库或上游接口重新拉取,顶多是出现短暂回源,影响可控。
- 有数据库兜底的准存储数据:订单状态、最新库存、用户积分等。数据库里有一份,但Redis里的这份被高频读写,一旦丢失,要么数据库压力激增,要么出现一段时间的数据不一致。
- 只有Redis一份的独有数据:分布式锁的持有记录、限流滑窗计数、验证码、临时Token、未读消息数、正在编辑的草稿。这一类最危险,丢了就是真丢了,没有任何地方能补回来。
很多Java服务的问题恰恰在于,把第三类数据也放在了"只管性能、不管可靠性"的配置里。比如验证码,生成之后写进Redis,设置5分钟过期,Redis一重启,所有拿着验证码等操作的请求全部失效,用户感受就是"我明明刚收到验证码,怎么提交就说不存在"。这类问题不是代码bug,是持久化设计缺失。
1.2 持久化防的三个典型故障场景
- 进程崩溃:Redis进程因为OOM被系统杀掉,或者误操作kill掉。此时内存里的数据瞬间消失,如果没有持久化文件,重启就是一个空实例。
- 宿主机器宕机:断电、物理机故障、虚拟机迁移失败。整个内存都没有了,比进程崩溃更彻底,必须靠磁盘上的持久化文件恢复。
- 主从架构下的连锁清空:主节点宕机后重启,如果持久化文件损坏导致启动为空,从节点同步时发现主节点数据是空的,会把自己也清空。整个集群一夜回到解放前。
第三个场景是运维中最容易忽略的暗坑,后面第五章会专门展开。这里先说清楚一个核心观点:持久化的价值不是"备份",而是把数据丢失窗口缩小到你能接受的范围。没有任何方案能做到绝对不丢,你要做的是选择窗口。
1.3 持久化和宕机恢复是两件事
写入持久化文件和用文件恢复数据,是两条不同的路径。很多架构师以为配置了RDB或AOF就万事大吉,但恢复阶段的文件加载顺序、损坏处理、主从之间的数据对齐、Java服务重连后的缓存预热,每一项都可能让恢复结果大打折扣。所以这篇文章从第三章开始,会把"怎么写"和"怎么恢复"分开讲,这样你排查问题时思路会更清晰。
2. RDB快照:全量备份机制的底层逻辑与参数权衡
2.1 一份二进制快照是如何生成的
RDB是Redis在某个时间点对内存全量数据做的一份压缩二进制快照,默认保存在dump.rdb文件里。快照不是直接把内存数据考出来那么简单,它依赖操作系统的fork机制:Redis进程调用fork生成一个子进程,子进程负责遍历内存数据并写入临时文件,写完后通过原子rename替换旧RDB文件,父进程继续服务读写请求。
这里最关键的是copy-on-write机制。fork出来的子进程和父进程一开始共享同一份物理内存页,子进程做快照时按顺序读取共享内存写入磁盘。问题来了:父进程在快照期间还在处理写请求,如果数据被改了怎么办?操作系统会在父进程写入前,先把被修改的内存页复制一份。父进程在副本上继续改,子进程手里的还是改动之前的旧版本。这样一来,子进程生成的快照就是fork时刻的一致数据视图,不会出现半新半旧的状态。
理解了这一点,很多参数就都好解释了。写请求越多,需要复制的内存页也越多,bgsave带来的额外内存开销就越大。如果实例已经用了8GB内存,又赶上写入高峰,瞬时内存开销可能接近甚至超过原有内存,物理内存不足时就会触发SWAP,Redis延迟飙升,这是大实例上RDB最典型的副作用。我在一线维护时一般把单实例内存控制在5GB左右,配合集群分片,就是为了给快照和恢复留出安全的资源余量。
2.2 save规则的权衡逻辑
默认配置里的三条规则是:
save 900 1 save 300 10 save 60 10000含义分别是:900秒内发生至少1次写操作就触发快照,300秒内发生至少10次写操作就触发快照,60秒内发生至少10000次写操作就触发快照。设计意图是:写入频率低时,允许快照间隔长一点;写入频率高时,快照自动加密,把丢失窗口压缩。
实际生产里,这套默认规则不一定适合你的业务。我见过一个写量很平均的服务,每分钟大概几百次写,默认规则下900秒才触发一次快照,丢失窗口长达15分钟,对业务来说完全不可接受。后来我调整为动态评估:先看业务能容忍丢多长窗口的数据,再反推快照频率。比如允许丢5分钟数据,就配置save 300 1;写量特别大时还可以配合后文说的AOF一起使用。
如果彻底不想用RDB,可以在配置里写save "",这样所有自动快照规则都会被禁用。但注意,手动执行bgsave、正常关停Redis、主从全量同步时仍然可能生成RDB文件,不是设了save空就完全不产生。
2.3 几个直接影响数据安全的配置项
stop-writes-on-bgsave-error这个参数很多人不重视。默认值是yes,含义是bgsave失败后Redis拒绝新的写请求。听起来很激进,但逻辑是对的:既然快照已经失败,再继续写入只会让数据丢失窗口越拉越大,不如暂时停写,等运维介入排查磁盘空间、权限、fork失败等根因。
dir和dbfilename要单独拎出来说。dir指定RDB和AOF文件的存放目录,dbfilename是RDB文件名。多实例部署时,我习惯在文件名里带上端口和实例角色,比如dump-6380-orders.rdb,避免多个实例共用目录时互相覆盖。
RDB文件本身的结构也值得了解:文件以"REDIS"魔数开头,后面跟着RDB版本号、辅助字段(比如最后一次保存的时间戳、使用的RDB版本)、实际数据集,以及文件末尾的CRC64校验和。加载时Redis会校验校验和,如果文件被截断或篡改,会直接报错拒绝启动。这是RDB的一种自我保护机制。
2.4 RDB使用中我踩过的三个坑
- bgsave失败不报警:默认情况下bgsave失败不会影响主流程,日志里可能只是一行错误。必须通过
info persistence里的last_bgsave_status字段主动排查,否则你可能在一个根本没有快照保护的环境里跑了好几天。 - 磁盘IO暴涨:bgsave是异步的,但子进程写盘仍然占磁盘带宽。如果磁盘是机械盘或者和业务日志共用一块云盘,快照期间Redis延迟会明显升高。我一般会把持久化文件单独放一块磁盘,并且让集群内多个实例的快照时间错开。
- fork时间过长:日志里的
fork time in microseconds如果经常是几百毫秒甚至秒级,说明内存页复制压力大。此时要重新评估实例内存大小,或者考虑升级到物理内存更充裕的机器。
3. AOF追加日志:写后日志模型下的数据安全边界
3.1 AOF记的是命令,不是数据
AOF全称是Append Only File,以追加写的方式记录每一条写命令。配置开启方式:
appendonly yes appendfilename "appendonly.aof"举个例子,执行set k v,AOF会追加一条set命令;执行incr k,AOF会再追加一条incr命令。文件里保存的不是最终值,而是产生最终值的操作序列。这也是AOF文件不断膨胀的根本原因——同样的key被改一百次,文件里就有一百条命令,最终只需要用一条set覆盖即可,所以必须有rewrite机制。
这和MySQL的redo log有本质区别。redo log是物理日志,记录数据页的修改,而且采用WAL(Write-Ahead Logging),先写日志再改数据。AOF是逻辑日志,记录命令本身,而且是写后日志:先执行命令修改内存,再把命令追加到AOF。这个设计的好处是:AOF不会记录失败的命令,也不存在写日志拖累主流程的问题。代价是:如果命令执行完、日志还没来得及落盘就宕机,这条命令对应的数据就丢了。丢失窗口由appendfsync策略决定。
3.2 appendfsync三种策略的取舍
appendfsync always # appendfsync everysec # appendfsync no- always:每条写命令都执行fsync强制刷盘,最安全,但写入吞吐下降明显,适合写量不大但对数据极度敏感的场景。
- everysec:每秒刷一次盘,极端情况下丢失1秒内写入的数据,是生产环境最常见的默认选择。
- no:由操作系统决定何时刷盘,宕机时丢失数据量不可控,一般不推荐。
这里要分清两个概念:把数据写入操作系统的page cache,和调用fsync真正落盘,是两回事。如果只是进程崩溃,page cache里的数据通常还在,丢失不会太大;如果整机断电,page cache里的内容会全部消失。everysec在这种场景下最多丢最后1秒的写操作,对大多数业务来说可以接受。
还有一个隐藏性能点要提醒:everysec模式下,如果上一秒的fsync还没完成,Redis会阻塞写请求等待fsync结束。写量特别大且磁盘性能差时,这个阻塞会成为性能瓶颈。我见过一个实例,AOF写入明明打开了,但每秒写入量超过磁盘刷盘能力,延迟从几毫秒涨到几百毫秒,最后只能把AOF挪到更高性能的磁盘上。
3.3 AOF膨胀与rewrite机制
只追加不合并,AOF文件迟早膨胀,所以Redis引入了rewrite。机制是:生成当前内存数据的最小命令集,替换旧AOF文件。比如一百次set同一个key,rewrite后只剩一条set命令,文件体积大幅缩小。
rewrite的触发有两种方式:手动执行bgrewriteaof,或者通过配置自动触发。自动触发条件:
auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb默认逻辑是:当AOF文件超过上一次rewrite后大小的100%(即翻倍)且超过64MB时触发。写量大的实例建议把percentage调低,比如50,让rewrite更频繁,避免AOF文件涨到离谱大小。
rewrite过程同样走fork子进程,而且多了一个AOF重写缓冲区的概念。子进程rewrite期间,父进程的新写入命令会放到缓冲区里,rewrite完成后追加到新文件末尾,保证一条不漏。但如果父进程写得太快而rewrite很慢,这个缓冲区会持续增长,额外内存占用可能很高,同样是大实例需要注意的隐藏开销。你在info persistence里能看到aof_rewrite_buffer_size,如果非常大,说明缓冲区压力很高。
3.4 AOF文件损坏时怎么办
AOF文件的数据格式是Redis序列化协议,理论上每一行都能独立解析。所以遇到尾部不完整,Redis可以截断后继续加载。配置文件里的aof-load-truncated默认就是yes,含义是启动时如果发现AOF尾部异常,自动截掉尾部后继续加载,适合进程崩溃导致最后一条命令写了一半的情况。
文件中间损坏则麻烦一些,官方提供的redis-check-aof工具可以修复。我的习惯是先保留原始文件的完整副本,再对副本执行修复,修复完成后确认文件可读再启动。直接启动损坏文件,可能会把坏数据解析成合法但错误的内容,这是比加载失败更隐蔽的风险。
4. 混合持久化:RDB快照和AOF日志的结合方案
4.1 为什么要混合:RDB快但丢数据,AOF全但加载慢
RDB的好处是文件小、加载快,坏处是两次快照之间存在丢失窗口;AOF的好处是丢失窗口极小,坏处是文件大、启动加载慢。设想一个写QPS很高的实例,纯AOF文件可能膨胀到数GB,启动时需要逐条解析几百万条命令,十几分钟都算正常。在大版本升级、批量重启、故障切换的场景里,这个时间完全不可接受。
Redis 4.0引入的混合持久化,配置项是aof-use-rdb-preamble yes(新版本多数发行版默认开启,我建议显式配置)。开启后,执行AOF rewrite时不再是生成纯命令文本,而是先生成一个RDB格式的二进制前缀,把当前全量数据一次性写入,再把rewrite期间的增量命令以AOF文本方式追加在后面。打开这样一个混合文件,前半段是RDB二进制块,后半段是AOF命令流。
4.2 混合文件的加载为什么变快了
Redis启动加载时,会先看AOF文件开头是不是"REDIS"魔数。如果是,说明是混合格式:先按RDB方式快速加载前缀的全量数据,再继续按AOF流式解析后面的增量命令。RDB格式的加载速度远高于逐条解析协议命令,整体恢复时间大幅缩短。
我做过一次对比测试:一个约5GB数据的实例,纯AOF文件大约7GB,进程重启后加载耗时约3分钟;混合持久化文件大约2.5GB,加载耗时约40秒。在集群有几十个分片同时重启的场景下,这个差距就是可用性和不可用性的差别。
4.3 混合方案的两个边界条件
第一,混合文件只在AOF rewrite时产生。如果你原来没开AOF,某天突然开启混合持久化,在第一次rewrite之前,磁盘上的AOF还是纯AOF格式,加载速度并不会立刻提升。需要手动执行一次bgrewriteaof,让它生成第一份混合文件。
第二,混合方案下auto-aof-rewrite-percentage变得更重要。因为RDB前缀决定后,追加的增量命令会不断累积,如果rewrite频率太低,文件后面那段AOF部分会越来越长,整个文件的加载速度会逐渐退化。实际运维中,我除了依赖自动触发,还会在业务低峰期手动执行bgrewriteaof,把RDB前缀和增量AOF的比例保持在一个健康范围。
第三,rewrite过程中如果掉电,新文件可能是不完整的。这时Redis会依赖旧文件和aof-load-truncated的兼容逻辑来处理。所以混合持久化不是银弹,它只是把RDB和AOF的各自优点尽量保留,但底层的fork、刷盘、损坏处理这些问题全部还在。
5. 宕机恢复完整链路:加载顺序、主从陷阱与数据校验
5.1 Redis启动时到底先读哪个文件
这个顺序很多人没搞清楚:如果appendonly配置为yes,Redis优先加载AOF(或混合)文件;如果没有开启AOF,才加载RDB文件。两个文件不会同时加载。原因很简单,AOF的丢失窗口比RDB小,数据准确性更高,所以AOF开启时会用AOF作为恢复依据。这里还隐含一个坑:你原来用RDB,某天开了AOF,如果此时还没有AOF文件,Redis会用当前RDB数据生成一份AOF;但从那以后,RDB文件在恢复时就不被使用了。如果团队里有人不知道这个变化,还在更新RDB相关配置,就会产生"配置了却没用上"的错觉。
5.2 进程崩溃后正确的恢复流程
一个典型场景:凌晨高峰期Java应用开始报Redis连接异常,进程崩溃了。此时不要急着直接重启Redis,按五步走:
- 先体检再启动:检查最近一次bgsave或rewrite是否成功,查看AOF/RDB文件大小是否合理,看日志里有没有加载失败的征兆。
- 备份原文件:把持久化文件copy一份到独立目录。这一步是保险,万一你启动时把损坏文件越修越烂,至少还有退路。
- 启动并观察日志:关注是否出现Redis ready、DB loaded等关键行,记录加载耗时。
- 对比数据量:用
info keyspace查看各db的key数量,和历史监控数据对比,评估到底丢了什么、丢了多少。 - 必要时启用兜底:如果丢失量超出预期,不要硬扛,考虑从从节点提升、从备份系统恢复,或者通知业务方回补数据。
5.3 主从架构下最经典的恢复事故
主从架构不等于高枕无忧,有一个事故模型在社区里反复出现:主节点宕机后,运维只想着赶紧重启,结果主节点的持久化文件已损坏,启动后Redis变成了空实例。从节点连上主节点,发现主节点数据比自己新,于是同步主节点状态——同步的结果是把自己的数据也清空。整个集群在一瞬间全部变空。
防止这个链条,有两个习惯必须养成:
- 主节点重启前一定要确认持久化文件完整。尤其是AOF文件如果显示为0字节,先别启动,查一下是不是文件丢失了。
- 建议从节点也开启持久化。很多团队的默认做法是"从节点只读、不开持久化",但真正可靠的设计往往是主节点开AOF,从节点额外开RDB或混合持久化,一旦主节点数据损坏,从节点还能提供一份完整数据兜底。
另一个相关参数是replica-serve-stale-data。默认yes,从节点和主节点断连后仍然响应读请求,返回的是陈旧的本地数据。如果业务无法接受读到旧数据,可以设置no,让从节点在断连期间直接拒绝读请求。这本质上是可用性和一致性的取舍,没有绝对的对错,要根据业务容忍度来定。
5.4 恢复完成后的三层数据校验
恢复完不是"能连上就行",我会做三层检查:
- key总量对比:通过
info keyspace,对比各db key数量与崩溃前监控数据的差异。 - 热点key抽查:挑几个业务最热、写入最频繁的key,确认存在且值符合预期。
- 业务探针验证:在Java服务里做一个定时任务,每隔几分钟写一个带时间戳的测试key。故障恢复后检查这个key是否还在、时间戳是否连续。这个探针的价值在于,能持续验证持久化和恢复链路是否真的可靠,而不是故障时临时写脚本。
如果你用Spring Boot集成Redis,恢复期间还要关注一件事:Java客户端连接池在Redis不可用时会大量抛出连接异常,如果服务没有做熔断降级,故障恢复后可能有大量请求积压,造成"二次雪崩"。建议给缓存操作加超时和降级策略,至少保证Redis不可用时核心数据库链路还能正常工作。
6. 生产选型与故障演练:能直接抄的配置和操作习惯
6.1 三种持久化方案的场景对照
| 业务特征 | 推荐方案 | 配置要点 | 备注 |
|---|---|---|---|
| 纯缓存,数据可重算 | RDB或关闭持久化 | save规则按容忍窗口调整 | 性能最好,但要接受缓存清空后的回源压力 |
| 准存储数据,要求少丢 | AOF everysec | appendonly yes+appendfsync everysec | 丢失窗口约1秒 |
| 数据极度敏感,写量不大 | AOF always | appendfsync always | 吞吐下降明显,慎用于高并发写 |
| 大实例、多分片、追求恢复速度 | 混合持久化 | aof-use-rdb-preamble yes | 兼顾完整性与恢复速度 |
6.2 一套可直接参考的生产配置
我在一个中等规模的Java服务上使用的配置如下,单实例峰值内存约4GB,写QPS约1.5万:
appendonly yes appendfsync everysec aof-use-rdb-preamble yes auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 1gb save 900 1 save 300 10 save 60 10000 stop-writes-on-bgsave-error yes这组配置的核心逻辑是:AOF做主要可靠性保障,RDB做额外快照层,混合持久化保证重启速度。恢复时Redis优先加载AOF,加载的是混合格式文件,速度不比纯RDB慢太多,数据完整性又远好于纯RDB。stop-writes-on-bgsave-error保持yes,宁可停写也不要无保护写入。
6.3 容易被误判的几个问题
- "Redis只是缓存,不需要持久化":如果业务能接受数据清空后的缓存雪崩,确实可以关。但一旦塞进去的是验证码、锁、计数,就必须开持久化。
- "打开AOF性能会崩":实测下来,everysec对性能影响相对有限,真正影响大的是always。不要在没压测的情况下盲猜,用实际写压测数据说话。
- "RDB和AOF同时开,数据是两份,占用双倍空间":不是。两种机制只是记录方式不同,文件是独立的两份,但恢复时只加载AOF,RDB文件不会和AOF合并。双份空间确实是事实,但这不是数据翻倍,单份内存数据对应两份不同格式的记录。
- "AOF文件可以定期删减":不能手动截断,只能通过rewrite让文件缩回最小状态。如果AOF异常膨胀,先排查是不是rewrite触发了但一直失败。
- "RDB文件好多年没更新,说明实例很稳定":这是危险信号。请立刻检查
last_bgsave_status和日志,说不定快照早就失败,只是没报警。
6.4 故障演练的落地建议
持久化方案不是配好就完了,建议每季度做一次故障演练,步骤很简单:
- 在测试集群执行
kill -9模拟进程崩溃,注意这模拟的是最坏情况,Redis没机会做正常关停的快照。 - 重启Redis,记录加载耗时、恢复后的key数量、日志告警。
- 把实际丢失的数据量和预估值对比,验证丢失窗口是否和设计一致。
- 再做一次"删除AOF文件后启动"的演练,确认你手上的备份机制能兜底。
- 观察Java服务重连、缓存预热、熔断降级是否按预期工作。
这里要特别提醒:很多团队测试的时候都用redis-cli shutdown正常关停,这样Redis会主动做一次快照,数据看起来总是完整的。但真实故障往往是断电、被kill掉,根本没机会做正常退出处理,所以一定要用kill -9这种极端方式做演练,才能真正暴露持久化设计的问题。
最后分享一个小技巧:无论采用哪种持久化方案,我都建议周期性对持久化文件做一次"离线加载测试"。用一个临时端口、空目录启动一个实例,加载同一份AOF/RDB文件,确认文件没有损坏、加载耗时在可接受范围内。这个动作不影响线上,却能提前发现很多恢复时才会暴露的问题。持久化文件写出来不是终点,真正可靠的意思是:你能用它完成一次预期内的恢复。