凌晨两点半,值班手机响了。某业务线的订单接口大面积超时,一层层排查下去,最后定位到缓存层的一台 Redis 实例进程直接消失了。运维同学重启实例,DBSIZE从崩溃前的一千两百万掉到八百七十万,三百多万个键凭空蒸发。更难受的是,有人在重启前顺手清了一下日志目录,把旧的 AOF 文件也带走了。那晚我们花了四个小时,才从一台延迟较高的从节点和一份前一天的全量快照里,把数据拼回到可接受的程度。
分布式缓存这东西,平时你几乎感觉不到它的存在,一旦崩了,Redis 数据恢复这件事就会把所有设计上的偷懒一次性摊开在你面前。这篇内容不讲“缓存穿透击穿雪崩”那套入门概念,专门聊崩溃之后的数据恢复:持久化文件到底存了什么、恢复的边界在哪里、什么情况下能救回来、什么情况下只能认。适合三类人看:写过缓存代码但没真正处理过线上崩溃的;做了备份但从没演练过恢复的;以及正在系统学分布式缓存那套 24 讲课程、想先把恢复这条线捋出实操框架的。全文基于常见生产实践做梳理,具体参数请结合你自己的数据规模和部署形态调整。
1. 崩溃现场:先判断丢了什么,再谈怎么救
1.1 三种崩溃现场,损失范围完全不是一回事
很多人一说“Redis 崩了”,脑子里浮现的画面是一样的,实际上三种现场的损失范围差得很远,处理顺序也完全不同。
进程级崩溃是最轻的一种。OOM Killer 杀掉进程、段错误、被误 kill,这类情况下内存数据全没了,但磁盘上的持久化文件是完好的。你要做的就是把文件重新加载回来,损失的是最后一次落盘之后那段时间的写入。这种场景恢复成功率最高,也最适合拿来练手。
主机级宕机要复杂一档。断电、内核 panic、宿主机故障,内存数据同样全丢,但磁盘上的文件可能处于“写了一半”的状态。尤其是 AOF 的everysec策略,理论上最多丢一秒,实际可能因为页缓存还没刷到磁盘而丢更多。更麻烦的是文件尾部的完整性,需要靠redis-check-aof这类工具去校验和修复。
存储级损坏是最难处理的。磁盘坏道、文件系统错乱、误删误格式化、容器卷挂载丢失。这种情况下持久化文件本身不可信,你只能依赖异地备份或者从节点。我见过一次容器重启后数据卷没挂上,Redis 在空目录里高高兴兴跑了一整天,第二天才发现数据全在新卷上,旧卷还躺在另一块盘里。
判断现场类型只需要看两个信息:进程退出原因(dmesg、容器退出码、Redis 日志尾部)和持久化文件的时间戳与大小。如果文件 mtime 停在崩溃前几分钟、大小正常,基本属于前两类;如果文件不存在或者大小为 0,那就要走备份路线了。
1.2 持久化机制,决定了恢复的天花板
Redis 的数据恢复能力,本质上是被持久化机制锁死的。你选了哪种持久化、参数怎么配,直接决定了崩溃后能捞回多少数据,这个因果关系必须先理清楚。
打个生活化的比方。RDB 是拍照片,每隔一段时间给内存拍一张全家福,照片之间的动作全部丢失。AOF 是写日记,每一条写命令都记下来,恢复的时候把日记从头念一遍,动作就复原了。混合持久化是照片加日记,先给你一张照片垫底,后面的变化再用日记补,兼顾加载速度和数据完整度。
- RDB 的优势是文件紧凑、加载快,适合做冷备和全量同步;劣势是两次快照之间的写入全部丢失,最坏情况可能丢几分钟的数据。
- AOF 的优势是数据完整度高,
everysec策略下通常只丢一秒左右;劣势是文件大、回放慢、尾部损坏需要修复。 - 混合持久化是 4.0 之后的主流选择,重写时前半段用 RDB 格式,后半段接增量 AOF,加载时先读 RDB 再回放 AOF,速度接近 RDB,完整度接近 AOF。
这里有个特别容易被忽略的点:当 RDB 和 AOF 同时开启时,Redis 启动会优先加载 AOF,因为 AOF 通常更完整。这意味着你如果只想恢复某个历史时刻的 RDB 快照,必须先把 AOF 关掉或者把 AOF 文件移走,否则你放进去的 RDB 根本不会被读。
1.3 恢复目标:先把 RPO 和 RTO 写清楚
没有目标的恢复就是瞎忙。两个指标必须先定下来,后面所有参数都是为它们服务的。
RPO(Recovery Point Objective)是你最多能接受丢多少数据,通常用时间衡量,比如“最多容忍丢 5 秒的写入”。RTO(Recovery Time Objective)是故障发生后多久必须恢复服务,比如“15 分钟内缓存层要能用”。
| 业务类型 | 典型 RPO | 典型 RTO | 建议持久化组合 |
|---|---|---|---|
| 会话、验证码、限流计数 | 30 秒以内可接受 | 5 分钟内 | AOFeverysec+ 每日 RDB 冷备 |
| 商品详情、配置缓存 | 5 分钟可接受 | 15 分钟内 | RDB 高频快照 + 从节点 |
| 订单状态、库存扣减辅助 | 尽量不丢 | 30 分钟内 | AOFeverysec+ 混合持久化 + 跨机房备份 |
| 纯热点加速、可回源重建 | 可全丢 | 越快越好 | 只开 RDB 甚至不开持久化 |
这张表不是让你照抄,是让你意识到一件事:不是所有缓存都值得用最重的方案去保。有些数据丢了直接回源数据库重建就行,为了它把 AOF 开成always,每条命令都 fsync,性能掉一半,得不偿失。先想清楚哪些数据丢了会真的疼,再决定给谁上重装备。
2. 持久化文件长什么样,参数怎么调才不埋雷
2.1 RDB 快照:触发条件与 fork 的真实代价
RDB 的触发有三条路:配置文件里的save规则、手动BGSAVE、以及主从全量同步时主库自动触发的快照。前两条是你能控制的,第三条经常被忽略,导致主从同步期间主库突然 fork 一次,延迟飙上去。
# 常用的一组合规配置 save 900 1 save 300 10 save 60 10000 stop-writes-on-bgsave-error yes rdbcompression yes rdbchecksum yes dbfilename dump.rdb dir /data/redis/datastop-writes-on-bgsave-error yes这一项争议最大。它的含义是:如果最近一次 BGSAVE 失败,Redis 会拒绝所有写命令。很多人觉得这是保护机制,实际上它救过不少人的命——因为 BGSAVE 失败往往意味着磁盘满了或者权限错了,如果不拦写,你会以为一切正常,直到真崩了才发现几个月没成功落盘过。所以我建议保持yes,同时把 BGSAVE 失败的告警接出来,别让它悄悄失败。
fork 的代价必须说清楚。BGSAVE 会 fork 一个子进程,fork 本身要复制页表,内存越大耗时越长,经验上每 GB 内存大约几毫秒到几十毫秒。这个过程主进程是阻塞的,二十 GB 的实例 fork 一次卡住二十毫秒很常见。子进程写快照期间,主进程的写操作会触发写时复制(COW),最坏情况下内存占用会接近翻倍。
所以我的建议是:单实例内存别超过 16GB,超过就拆成多个实例。另外务必关掉透明大页(THP),THP 会让 COW 复制的粒度从 4KB 变成 2MB,写放大严重,fork 之后延迟抖动会非常明显。
# 关闭 THP,临时生效 echo never > /sys/kernel/mm/transparent_hugepage/enabled echo never > /sys/kernel/mm/transparent_hugepage/defrag2.2 AOF 三种落盘策略,选错一次就够难受
AOF 的核心参数只有一个:appendfsync。三个取值对应三种完全不同的可靠性模型。
| 策略 | 行为 | 最坏丢数据 | 性能影响 |
|---|---|---|---|
always | 每条写命令都 fsync | 最多丢 1 条命令 | 吞吐下降明显,通常只有everysec的三分之一到一半 |
everysec | 后台线程每秒 fsync 一次 | 约 1~2 秒 | 影响很小,生产主流选择 |
no | 交给操作系统决定,通常 30 秒 | 可能丢 30 秒 | 几乎无影响,但不可控 |
everysec为什么是“1~2 秒”而不是精确的 1 秒?因为 AOF 缓冲区每秒刷一次盘,如果在第 0.99 秒崩溃,第二秒的还没刷,同时上一次刷盘之后写入的数据也在缓冲里,叠加起来就是接近两秒。这个细节在算 RPO 的时候一定要算进去,别跟业务方拍胸脯说“最多丢一秒”。
还有一个容易踩的坑:no-appendfsync-on-rewrite。它的默认值是no,意思是重写期间仍然执行 fsync。如果磁盘 IO 本来就吃紧,重写时主线程会因为等 fsync 而被阻塞,出现秒级卡顿。把它设成yes可以避免卡顿,代价是重写期间的数据可能丢失。这是个典型的可靠性换稳定性的取舍,我的做法是:磁盘是 SSD 就保持no,机械盘或者云盘 IO 抖动大的环境设成yes并接受少量丢失风险。
2.3 AOF 重写与混合持久化:文件到底长什么样
AOF 会无限膨胀,所以需要重写。重写的逻辑不是“读旧 AOF”,而是“读当前内存,生成一份能重建当前数据集的最小命令集”。这意味着重写出来的文件可能比原文件小得多。
混合持久化由aof-use-rdb-preamble yes控制,Redis 4.0 之后默认开启。重写时,子进程把当前内存以 RDB 格式写到新 AOF 的开头,重写期间的新写入追加到后面。加载时先读 RDB 部分,再回放 AOF 部分。
Redis 7.0 之后 AOF 的组织方式变了,从单文件变成了多文件,这个变化对恢复流程影响很大,必须单独说。
# Redis 6 及之前:单文件 /data/redis/data/ └── appendonly.aof # Redis 7 及之后:多文件 + manifest /data/redis/data/appendonlydir/ ├── appendonly.aof.manifest ├── appendonly.aof.1.base.rdb └── appendonly.aof.1.incr.aofappendonly yes appenddirname "appendonlydir" appendfilename "appendonly.aof" aof-use-rdb-preamble yes auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb aof-load-truncated yes这里最关键的一点:Redis 7 恢复时不能只拷那个.incr.aof,必须把整个appendonlydir目录连同 manifest 一起拿过去。manifest 里记录了 base 文件和 incr 文件的对应关系,缺一个文件或者版本号对不上,启动就会报错。我见过有人只拷了 base.rdb,结果恢复出来的是重写那一刻的数据,后面几小时的增量全没了,还以为自己恢复了。
2.4 磁盘和内存容量,按公式算而不是拍脑袋
容量不够是恢复失败的头号原因,而且往往在半夜最慌的时候才发现。两个数必须提前算。
磁盘需求:AOF 开启时,磁盘占用大致是数据量的一点二到两倍,重写期间新旧两份同时存在,峰值接近三倍。加上 RDB 快照、日志、备份文件,建议预留数据量的四倍。
# 举例:数据集 20GB # AOF 常态占用:约 20GB × 1.5 = 30GB # 重写峰值:旧 AOF 30GB + 新 AOF 25GB = 55GB # 加一份 RDB 快照 8GB(压缩后) # 加日志与余量 # 建议磁盘 ≥ 80GB内存需求:恢复加载时,如果maxmemory设置小于实际数据量,且淘汰策略是allkeys-lru这类,Redis 会在加载完成后立刻开始驱逐,你以为恢复了,其实数据已经被删了一批。所以恢复环境的maxmemory要临时调大,或者先把策略改成noeviction,恢复完成、校验通过之后再调回正常值。
# 恢复期间临时配置 maxmemory 0 maxmemory-policy noeviction还有个隐藏成本:RDB 加载时,Redis 是逐条解析并插入的,内存占用会短暂超过最终值,因为哈希表扩容有负载因子。经验上预留百分之二十的余量比较稳妥。
3. 恢复实操:从持久化文件到可用实例
3.1 动手前必须先做的四件事
数据恢复最忌讳的就是“一上来就覆盖”。文件一旦被覆盖或者被 Redis 以新数据重写,就真的回天乏术了。以下四步是我每次恢复前的固定动作。
第一,保护现场。把整个数据目录打包复制到另一块盘,包括日志文件。这一步可能要多花几分钟,但它决定了你后面还有没有第二次机会。用cp -a保留权限和时间戳,别用mv。
第二,确认版本。查看故障实例的 Redis 版本和恢复目标实例的版本。RDB 格式只能向上兼容,不能向下兼容。Redis 7 生成的 RDB 文件(版本号 11)用 Redis 6 打开会直接报Can't handle RDB format version。版本不一致时,最稳的办法是先用相同大版本恢复,再通过主从同步迁移到新版本。
第三,停掉自动重写和写入。恢复期间最怕的就是实例在加载完的那一刻立刻触发 AOF 重写或者接受新写入,把旧文件覆盖了。先把客户端流量切走,配置里临时改成appendonly no,恢复完再打开。
第四,备份原始文件到异地。本地备份再稳也怕手滑,传一份到另一台机器或者对象存储,成本很低。
# 现场保护示例 TS=$(date +%Y%m%d%H%M) cp -a /data/redis/data /data/redis/backup_$TS cp -a /var/log/redis/redis.log /data/redis/backup_$TS/redis.log ls -lh /data/redis/backup_$TS3.2 纯 RDB 恢复:步骤最少,但校验不能省
RDB 恢复是最 straightforward 的路径,把文件放对位置、权限给对、启动就行。但“放对位置”这四个字坑过不少人。
# 1. 停掉目标实例 systemctl stop redis # 2. 确认配置里的 dir 和 dbfilename grep -E "^(dir|dbfilename)" /etc/redis/redis.conf # 假设输出:dir /data/redis/data dbfilename dump.rdb # 3. 清空目标目录里的旧文件(已备份过) rm -f /data/redis/data/dump.rdb rm -rf /data/redis/data/appendonlydir # 4. 拷入待恢复的 RDB cp /data/redis/backup_20240101/dump.rdb /data/redis/data/dump.rdb chown redis:redis /data/redis/data/dump.rdb chmod 660 /data/redis/data/dump.rdb # 5. 校验 RDB 文件完整性 redis-check-rdb /data/redis/data/dump.rdb # 6. 启动并观察日志 systemctl start redis tail -f /var/log/redis/redis.log启动日志里会有一行DB loaded from disk: 12.345 seconds,这个时间就是加载耗时。加载期间 Redis 是不响应请求的,所以大实例的恢复时间要提前算进 RTO 里。
redis-check-rdb不是万能的,它能发现结构性的损坏,但发现不了“文件完整但内容不对”。所以校验还得靠后面的数据比对。
权限问题在国内环境里特别常见。SELinux 开启时,即使文件属主和权限都对,Redis 也可能读不了,日志里会出现Permission denied但文件明明可读。临时排查可以setenforce 0,长期方案是给数据目录打上正确的 SELinux 上下文。
# 排查 SELinux 是否在拦 getenforce # 查看审计日志 ausearch -m avc -ts recent | grep redis3.3 AOF 恢复:尾部损坏是常态,修复要懂边界
AOF 的恢复流程比 RDB 多两步:判断是否截断、判断是否需要修复。崩溃时 AOF 尾部写了一半是极常见的情况,因为写文件不是原子的,最后一条命令可能只写了一半。
aof-load-truncated yes的作用就是应对这种情况:加载时发现尾部不完整,打个警告,加载到最后一个完整命令为止,正常启动。这个配置建议保持默认的yes,否则一次意外断电就可能让整个实例起不来。
如果日志里出现的是Bad file format reading the append only file,那就不是简单截断了,需要人工修复。
# Redis 6 及之前 redis-check-aof --fix /data/redis/data/appendonly.aof # Redis 7 及之后,要对 manifest 操作 redis-check-aof --fix /data/redis/data/appendonlydir/appendonly.aof.manifest修复工具会提示你它会截断到最后一个有效命令的位置,输入y确认。这里有个必须提醒的点:修复是有损的。它会丢弃最后一个不完整命令之后的所有内容,如果损坏发生在文件中段(比如磁盘坏道),后面所有数据都会被扔掉。所以修复前一定要先备份原文件,修完对比一下DBSIZE,评估损失是否可接受。
还有一类报错容易让人误判:Short read or OOM loading DB。这个不一定是内存不够,也可能是文件被截断过。先检查文件大小和预期是否吻合,再去看内存。
修复完成后,启动实例前记得把appendonly打开,并且确认 manifest 引用的文件都在目录里。
# 修复后检查目录完整性 ls -lh /data/redis/data/appendonlydir/ cat /data/redis/data/appendonlydir/appendonly.aof.manifest3.4 混合持久化与跨版本恢复的坑
混合持久化的文件前半段是 RDB,后半段是 AOF,这个结构导致它有一个特殊问题:前半段损坏会导致整个文件不可用,后半段损坏可以截断。所以校验时要分两段看。
# 判断 AOF 文件是否为混合格式 head -c 5 /data/redis/data/appendonlydir/appendonly.aof.1.incr.aof # 输出 REDIS 说明是 RDB 格式开头(混合持久化的 base 部分)跨版本恢复是另一个高频问题。从高版本往低版本恢复基本走不通,从低版本往高版本一般可以,但要注意几个细节。
- Redis 4 之前的 AOF 没有混合格式,升级后如果开了
aof-use-rdb-preamble,第一次重写会变成混合格式,之后的文件就不能再用老版本读了。 - Redis 7 的多文件结构,老版本的
redis-check-aof不认,必须用 7 及以上的工具。 - 集群模式下,节点数据文件里包含槽位信息,跨集群恢复时如果槽位分配变了,需要额外处理。
我个人的做法是:恢复环境永远和故障环境保持同一大版本,恢复成功、数据校验通过之后,再通过主从复制升级。多花一步,但省掉一堆玄学问题。
3.5 恢复后的校验:别看到 PONG 就以为完事
实例能启动、PING返回PONG,只能说明进程活着,不代表数据对了。校验要分层做。
# 第一层:整体规模 redis-cli -h 127.0.0.1 -p 6379 INFO keyspace # 输出示例:db0:keys=11800000,expires=2300000,avg_ttl=3600000 # 第二层:和预期值比对 # 崩溃前记录过 DBSIZE=12000000,现在 11800000,差 20 万,在 RPO 范围内,可接受 # 第三层:抽样业务键 redis-cli GET user:10086:profile redis-cli TTL order:20240101:8888 redis-cli LLEN queue:task:pending # 第四层:大 key 与过期键分布 redis-cli --bigkeys redis-cli --scan --pattern "session:*" | head -20INFO keyspace里的expires字段经常被忽略。如果恢复后过期键数量明显偏少,可能是过期时间在持久化过程中出了问题,或者你恢复的是 RDB 快照,而快照之后新设置的过期时间丢了。这个对会话类业务影响很大,因为过期时间丢了意味着键永远不会自动删除,内存会一直涨。
校验还有一个技巧:如果业务侧有对账逻辑,直接把缓存和数据库抽样比对。比如随机抽一万个用户 ID,分别从缓存和数据库读,比对字段值。这比任何工具都直观。
4. 高可用架构下的恢复:切换不等于没丢数据
4.1 主从切换后,新主可能比旧主少数据
很多团队觉得“我有主从,崩了切过去就行”,把高可用当成了数据安全的替代品。这是个危险的误解。主从复制的本质是异步的,主库写完内存就返回成功,什么时候同步到从库是不确定的。
主库崩溃的瞬间,那些已经返回给客户端成功、但还没同步到从库的写入,就永久丢失了。切换之后,从库升主,数据就少了这一截。这个窗口在低延迟网络下可能只有几毫秒,在跨机房场景下可能几百毫秒,高峰期积压时甚至几秒。
缓解手段有三个,按代价从低到高排。
第一,调大复制积压缓冲区。repl-backlog-size默认只有 1MB,高写入量下几秒就写满了。一旦写满,从库短暂断连后就需要全量同步,而不是增量。按写入速率乘以可容忍断连时长来算。
# 假设峰值写入 5MB/s,希望容忍 120 秒断连 # 5MB/s × 120s = 600MB,留余量设 1gb repl-backlog-size 1gb第二,配置最小从库数量。让主库在没有足够健康从库时拒绝写入,用可用性换一致性。
min-replicas-to-write 1 min-replicas-max-lag 10这两行的含义是:至少有一个从库的延迟在 10 秒以内,主库才接受写入。代价是网络抖动时可能短暂拒绝服务,所以只适合对数据一致性要求高的场景。
第三,关键写入用WAIT命令。客户端写完主库后,调用WAIT 1 100,等待至少一个从库确认,超时 100 毫秒。这能把同步确认纳入业务逻辑,但每次写都多一个往返,性能影响明显,一般只用在极少数关键操作上。
4.2 哨兵切换的参数,配置错就是反复切
哨兵模式下,切换的触发和完成由一组参数控制,配错了会导致误切、反复切、或者切不过去。
sentinel monitor mymaster 10.0.0.11 6379 2 sentinel down-after-milliseconds mymaster 30000 sentinel failover-timeout mymaster 180000 sentinel parallel-syncs mymaster 1quorum设为 2 表示至少两个哨兵认为主库挂了才触发投票,但真正执行切换还需要多数派授权。三节点部署时,quorum 设 2 是合理的;设 1 容易因为单个哨兵网络抖动误判。
down-after-milliseconds是主观下线判定时间,设得太短,一次 GC 停顿或者网络抖动就触发切换;设得太长,真故障时恢复慢。三十秒是个常用值,但如果你有明确的延迟指标,可以按 P99 延迟的两到三倍来定。
parallel-syncs控制切换后同时向新主同步的从库数量。设成 1 最安全,因为新主刚上任,多个从库同时全量同步会把它压垮;设大可以加快恢复速度,代价是期间新主可能不可用。
哨兵切换后最常见的问题不是切换本身,而是旧主恢复后不知道自己已经降级。如果旧主被网络隔离,恢复后仍以为自己是主库,就会产生双主,数据分裂。防护手段是在主库配置里也写上min-replicas-to-write,隔离期间写不进去,避免脏数据。
4.3 集群模式:槽位和节点配置文件的处理顺序
集群模式的数据恢复,除了持久化文件本身,还要处理槽位分配和节点元数据。这里有个文件绝对不能随便删:nodes.conf(由cluster-config-file指定)。
cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 15000 cluster-require-full-coverage nonodes.conf里记录了节点 ID、槽位归属、已知节点列表。恢复单节点时,如果直接从备份里恢复nodes.conf,节点 ID 和集群里其他节点的认知会不一致,需要CLUSTER MEET重新握手。如果删掉它,节点会以新 ID 加入,占用过的槽位需要手动重新分配。
我的恢复顺序是这样的:先停所有节点,再逐个恢复数据文件和nodes.conf,然后按原顺序启动,最后用redis-cli --cluster check检查集群状态。如果有槽位缺失,用redis-cli --cluster fix修复。
cluster-require-full-coverage建议设成no。默认值yes意味着只要有一个槽位不可用,整个集群就拒绝服务,恢复期间部分节点还没起来的时候,整个集群会完全不可用,压力全压到数据库上。设成no之后,只有受影响的槽位不可用,其他数据照常读写。
5. 常见问题速查与排查实录
5.1 启动报错速查表
| 报错信息 | 常见原因 | 处理动作 |
|---|---|---|
Can't handle RDB format version | 用低版本 Redis 打开高版本 RDB | 换用同版本或更高版本恢复,再通过主从降级 |
Short read or OOM loading DB | 文件截断,或内存不足 | 先查文件大小是否完整,再查maxmemory与系统内存 |
Bad file format reading the append only file | AOF 尾部或中段损坏 | 备份后redis-check-aof --fix,评估截断损失 |
Permission denied但文件可读 | SELinux 或容器挂载权限 | getenforce排查,修正上下文或挂载参数 |
Fatal error loading the DB: Invalid argument | 数据目录指向错误,或符号链接失效 | 检查dir配置与实际挂载路径 |
启动成功但DBSIZE远小于预期 | 加载了错误的文件(AOF 优先于 RDB) | 确认appendonly状态,检查目录里有哪些文件 |
MISCONF Redis is configured to save RDB snapshots | BGSAVE 失败,写被拒绝 | 检查磁盘空间、目录权限、fork 是否被限制 |
5.2 三个真实踩过的坑
坑一:以为恢复了,其实加载的是空文件。有次容器重启后数据目录挂了新卷,Redis 在空目录里启动,自动生成了一份空的dump.rdb和新的 AOF。运维同学执行恢复时,cp命令把旧的dump.rdb拷进去了,但忘了 AOF 目录还在,Redis 优先加载了那个空 AOF,DBSIZE为 0。折腾了一个小时才发现问题。教训是:恢复前先ls -la看清目录里所有文件,尤其是 AOF 目录,别只盯着 RDB。
坑二:maxmemory设小了,恢复完开始自动驱逐。恢复环境是从模板创建的,maxmemory沿用了模板的 8GB,实际数据 12GB。加载过程中 Redis 没报错,加载完成后立刻触发了allkeys-lru驱逐,等我们去校验的时候,已经删掉了几百万个键,而且是从最近最少使用的开始删,业务侧感知是“某些用户数据突然没了”。后来把所有恢复环境的maxmemory临时设成 0,恢复完再调回。
坑三:AOF 修复把中间数据修没了。一个实例的 AOF 文件在中段出现了损坏(后来确认是磁盘坏道),redis-check-aof --fix把损坏点之后的内容全截断了。因为损坏点在文件中间,等于丢掉了一半的增量数据。后来复盘,正确做法是先用redis-check-aof不带--fix只做检查,看看损坏位置大概在文件的百分之几,如果靠前,应该优先考虑从节点或者 RDB 加增量重建,而不是直接修。
5.3 恢复时间怎么估,别在大半夜现算
RTO 是承诺给业务的,必须提前估。两个阶段的耗时差别很大。
- RDB 加载:主要受磁盘读速度和单核解析速度限制。经验值在每秒几百 MB 到 1GB 之间。10GB 的 RDB 大约十到三十秒。
- AOF 回放:逐条执行命令,速度取决于命令复杂度。简单 SET 每秒几十万条,涉及大集合的操作会慢很多。10GB 的 AOF 回放几分钟很正常。
- 集群恢复:还要加上节点握手、槽位同步、主从全量同步的时间。从库全量同步一个 10GB 的实例,加上传输时间,五到十五分钟是常态。
把这些数字记在故障预案里,比临时拍脑袋靠谱得多。
6. 恢复演练与日常防守:把功夫下在崩之前
6.1 备份策略:三份副本、两种介质、一份异地
持久化不等于备份,这个认知差是很多事故的根源。持久化是为了快速恢复服务,备份是为了应对“持久化文件也没了”的极端情况。两者不能互相替代。
我沿用的一套简单原则:至少三份数据副本,存放在两种不同介质上,其中一份在异地。具体到 Redis:
- 本机保留 RDB 快照和 AOF,用于快速恢复。
- 每天定时把 RDB 快照传到独立的备份存储,保留最近三十天。
- 每周做一次全量备份归档,保留三个月。
- 重要业务再额外同步一份到另一个可用区。
传输前先做压缩和校验,sha256sum前后对比,避免传了个半截文件还以为备份成功了。备份任务本身要有告警,连续失败两次必须通知到人。
6.2 演练清单:不演练的备份等于没有备份
我见过太多团队,备份任务天天跑,绿油油的,真出事的时候发现备份文件全是空的。原因可能是备份脚本里redis-cli SAVE一直失败但从没检查返回值,或者压缩命令报错但脚本继续往下走。
演练要按季度做,清单如下。
- 从备份存储随机拉一份最新的 RDB。
- 在一台干净的、配置与生产接近的机器上恢复。
- 记录从开始到
PING返回PONG的总耗时。 - 记录
DBSIZE、INFO keyspace,与备份时的记录比对。 - 抽样二十个业务键,逐个校验内容和 TTL。
- 用
redis-cli --bigkeys扫一遍,确认大 key 都在。 - 把耗时和数据量写进文档,更新 RTO 估算。
- 演练完销毁环境,不要留着变成新的“生产”。
这套流程走一遍大概两小时,一个季度一次完全值得。演练的目的不是证明“我能恢复”,而是发现“原来这里还有问题”。
6.3 几个不太起眼但很值钱的细节
监控 BGSAVE 的成功状态。rdb_last_bgsave_status这个指标要接告警,连续失败就是磁盘或者权限出了问题。同时监控rdb_changes_since_last_save,如果这个值持续上涨但rdb_last_save_time不更新,说明快照一直没成功。
AOF 重写期间监控延迟。重写会带来 IO 压力,aof_rewrite_in_progress为 1 时如果latency_percentiles_usec明显升高,说明磁盘撑不住,考虑换盘或者调no-appendfsync-on-rewrite。
给数据目录单独挂盘。日志和数据放同一块盘,日志写满把盘撑爆导致持久化失败,这种事一点都不稀奇。分开挂载,各自限额。
容器环境确认数据卷的挂载方式。用mount | grep redis或df -h在容器内确认,别只看编排文件。有次编排文件写的是持久卷,实际因为路径拼错挂了个临时目录,重启即丢。
记录每次变更的基线数据。每次大版本升级、扩容、参数调整之后,记一下DBSIZE和内存占用。真出事的时候,你才知道“恢复正常”到底是什么样子。
分布式缓存这套东西,写代码的时候感觉很简单,GET、SET两个命令就完事了。真正拉开差距的地方,全在崩溃之后的那几个小时里。数据恢复这件事,平时多花两小时演练,出事的时候就少熬一个通宵。我个人在这些年的实践里最深的体会是:恢复方案的价值不在于它多精妙,而在于它是不是被人真的跑过一遍。纸上推演得再顺,第一次实操总会冒出几个没预料到的权限、路径、版本问题。把演练当成常规动作,比事后补救便宜太多。