
1. 先搞清楚“崩溃”到底崩在哪Redis 数据恢复的四种真实场景凌晨两点被告警电话叫醒群里一句“Redis 挂了数据好像没了”这大概是每个维护过缓存层的人都经历过的画面。Redis 崩溃后的数据恢复这件事最怕的不是崩而是崩完之后手忙脚乱地按了重启把一个本来还能救回来的现场直接冲掉了。我见过太多次这样的操作机器重启、进程拉起来、发现数据少了再去翻日志AOF 文件已经被新的写入覆盖了一部分原本能救回来的东西彻底没了。Redis 作为一个内存数据库它的数据持久化从来不是“有或没有”的二元问题而是一个丢失窗口有多大、你能接受多大的连续问题。崩溃之后能不能恢复、恢复到哪个时间点、恢复过程要多久这三件事在你写下第一行appendonly yes的时候就已经基本决定了。所以聊恢复之前必须先把“崩溃”这个笼统的词拆开——不同的崩溃形态恢复路径完全不同操作手法也完全不一样。适合读这篇的人我假设你至少知道 Redis 有 RDB 和 AOF 两种持久化方式跑过redis-cli看得懂INFO的输出。如果你是完全的新手也没关系我会把每个关键参数为什么这么设讲透你照着抄配置之前先理解它在救什么。1.1 进程被杀与整机掉电恢复路径完全不是一回事kill -9掉一个 Redis 进程和机房断电、宿主机宕机看起来都是“服务没了”但对数据的影响差得很远。进程被杀的时候操作系统还在跑内核的页缓存还在AOF 缓冲区里那些还没落盘的内容有一部分会被内核在进程退出时刷下去取决于appendfsync策略和缓冲区的实际位置。掉电就不一样了页缓存里没落盘的东西全没了甚至磁盘上正在写的那个扇区都可能是半截的。这一点直接决定了你的排查起点。进程被杀先看appendonly.aof的尾部是不是完整的命令整机掉电除了看 AOF 尾部还得考虑文件系统层面的损坏尤其是用了某些日志模式的文件系统redis-check-aof报出来的错误可能是文件系统写坏了而不是 Redis 自己写坏的。还有一个容易被忽略的点容器化部署下kill信号可能被编排系统发给了 PID 1 而不是真正的 Redis 进程导致 Redis 走的是被强杀的路径压根没触发SHUTDOWN时的保存动作。我在一个容器环境里排查过一次“重启后数据回到昨天”的问题最后发现是停止脚本没把信号透传进去Redis 每次都是被 SIGKILL 干掉的save配置里的规则压根没机会执行。1.2 主节点自己爬起来和从节点被顶上来是两码事单机崩溃恢复的目标是“把这台机器上的数据找回来”。集群或者主从架构下崩溃问题会变成“哪台机器的数据是最新的”。这两件事的处理逻辑完全不同很多事故就出在把后者当成前者处理了。主从复制是异步的。主节点处理完写命令、返回客户端成功然后才把命令传播给从节点。也就是说客户端收到OK的那一刻从节点可能还没收到这条命令。主节点一旦宕机从节点被提升为新主那些“主节点写了但还没同步过去”的数据就永久丢失了。这个丢失量取决于repl-backlog-size、网络 RTT 和当时的写入压力在写入密集的场景下几百毫秒的延迟就可能是上千条命令。更麻烦的是脑裂。主节点因为网络分区被判定为下线哨兵把从节点提成新主客户端往新主写了一批数据随后老主节点网络恢复它并不知道自己已经被替换如果配置不当老主节点被降级时会清空自己的数据去全量同步新主那批在新主上写的数据就没了。所以恢复之前先确认一件事现在这个“主”是原来的主还是被提上来的从它的master_repl_offset是多少INFO replication里的slave_repl_offset和master_repl_offset差多少这几个数就是你判断“丢了多少”的直接依据。1.3 数据“看起来丢了”其实可能只是读错了地方还有一种情况特别冤数据根本没丢是恢复之后读不到然后被误判成丢数据接着一通乱操作把真数据覆盖了。常见的几种恢复时加载了错误的配置文件dir指向了另一个目录Redis 从空目录启动看起来“数据全没了”其实老文件静静躺在原来的路径下。主从切换后客户端还连着老地址老节点被清空后读到空值。集群模式下 key 被重新分片客户端没有正确处理MOVED重定向读到了错误的槽位。恢复后没有预热大量请求直接穿透到数据库误以为是缓存数据丢了。我的习惯是任何“数据丢失”的告警先做三件事再动手确认当前实例的dir和dbfilename、确认加载的是哪个持久化文件、确认当前实例的角色和 offset。这三步花不了两分钟能避免绝大部分误操作。2. 持久化机制拆解RDB 和 AOF 各自能救回多少数据要谈恢复就得先把“能恢复到什么程度”这件事量化。RDB 和 AOF 不是好坏的取舍而是两种不同的丢失模型RDB 丢的是时间窗口AOF 丢的是命令条数。理解这个区别你才知道该怎么为不同的业务配不同的策略。2.1 RDB快照的代价是一个明确的时间窗口RDB 的本质是 fork 一个子进程把当前内存里的数据全量写成一个二进制文件。fork 的瞬间子进程看到的是父进程内存的一个一致快照靠写时复制 COW 实现所以 RDB 文件里的数据是某一时刻的完整状态内部是自洽的不会出现“一半新一半旧”的情况。这是 RDB 最大的优点恢复出来的数据一定是一个逻辑上完整的时点。代价也很明确从最后一次快照到崩溃之间的所有写入全部丢失。这个窗口由save规则或者你手动BGSAVE的时机决定。比如经典的save 900 1 / save 300 10 / save 60 10000意味着最坏情况下你要丢 15 分钟的写入——如果这 15 分钟里只有 1 个 key 被改那这 1 个改动就丢了如果有 9999 个改动但没到 10000同样不触发。还有一个隐藏成本fork 的耗时和内存占用。Redis 实例内存越大fork 时复制页表的耗时越长这段时间主进程是阻塞的。20GB 的实例在普通机器上 fork 可能阻塞几百毫秒到一秒这期间所有请求都在排队。COW 还会导致内存占用上涨写入越密集涨得越快如果物理内存不够子进程可能被 OOM killer 干掉rdb_last_bgsave_status变成err这次快照就白做了。所以用 RDB 做恢复方案你得接受两件事丢一段时间的写入以及快照不一定每次都成功。生产上单靠 RDB 兜底是不够的它更适合做冷备份和全量迁移而不是崩溃恢复的主力。2.2 AOF三种 fsync 策略下的丢失窗口要算清楚AOF 记录的是每一条写命令重启时重放这些命令来重建数据。它把“丢多少”这个问题转化成了“有多少命令还没落盘”。appendfsync三个取值对应三种完全不同的行为很多文档只说结论不说原理我这里把每种的实际含义摊开讲。策略行为理论最大丢失实际表现always每条命令写完都 fsync1 条命令吞吐掉到磁盘 IOPS 级别普通机械盘基本不可用everysec每秒由后台线程 fsync 一次约 1 秒的写入主流选择极端情况会到 2 秒no交给操作系统决定不可控可能几十秒只在能容忍大量丢失的场景用everysec为什么“极端情况会到 2 秒”因为负责 fsync 的是一个后台线程主线程把命令写进 AOF 缓冲区后直接返回。如果上一次 fsync 还没完成磁盘卡住了后台线程会跳过这一轮主线程最多阻塞 2 秒后强制同步并报错。也就是说磁盘出现抖动时丢失窗口会从 1 秒放大到 2 秒同时主线程被打断INFO persistence里的aof_delayed_fsync计数会涨。这个指标我建议纳入监控它比单纯的aof_last_write_status更早反映磁盘问题。用数字说话假设峰值写入 2 万条命令/秒everysec策略下最坏丢 2 秒就是 4 万条命令。这些命令如果涉及订单状态、库存扣减就是实打实的业务损失。所以真正重要的不是“AOF 比 RDB 好”而是要算清楚你的业务能接受丢多少然后倒推该用哪种策略、需不需要额外的补偿机制。2.3 混合持久化为什么现在基本是默认答案Redis 4.0 之后引入了混合持久化aof-use-rdb-preamble yesAOF 重写时不再生成纯命令文本而是前半段写成 RDB 格式的全量快照后半段追加增量的命令。重启加载时先快速加载 RDB 部分再重放后半段增量命令。这个设计解决的是 AOF 恢复慢的老问题。纯 AOF 恢复要逐条重放命令1000 万条命令重放可能要几分钟甚至十几分钟这期间服务是不可用的。混合持久化把这段耗时压到了秒级因为 RDB 是二进制格式加载速度比解析文本命令快一个数量级。代价是兼容性混合格式的 AOF 文件用老版本 Redis 打不开用普通文本编辑器看前半段是乱码排查问题时没法直接tail看最近写了什么。我倒觉得这个代价可以接受——出问题时用redis-check-aof工具处理比手工肉眼看文件更靠谱。真要定位最后写了什么可以从 RDB 之后的增量部分看那部分还是纯文本命令。注意开启混合持久化只在 AOF 重写时生效已经存在的纯文本 AOF 文件要等下一次性重写BGREWRITEAOF之后才会变成混合格式。别以为改了配置就立刻生效。Redis 7.0 之后 AOF 的实现又变了改成了多文件manifest base incr解决的是重写期间双写带来的 IO 放大问题。如果你的版本在 7.0 以上恢复流程会有细微差别损坏的不再是单个大文件而是某个 incr 文件redis-check-aof处理的对象和命令参数也有变化。排查前先INFO server看一眼redis_version别拿着 6.x 的经验去处理 7.x 的文件。3. 恢复实操从崩溃现场到数据回来的完整流程理论讲完下面是真正的手上功夫。我把一次完整的恢复拆成保护现场、选择恢复源、执行恢复、校验结果四段每一段都有具体的命令和判断依据。这套流程我在不同规模的实例上跑过很多次20GB 以下的实例基本能在半小时内处理完。3.1 第一步是保护现场不是急着重启崩溃之后最不该做的事就是直接拉起进程。因为 Redis 启动时会自动加载持久化文件如果配置是混合模式它加载完 RDB 部分还会重放 AOF这个过程可能对文件做修改或者触发重写。一旦原始文件被动了后续想从备份恢复或者做文件级修复就少了参照。正确的做法是先做文件级备份再动手。备份的时候注意两点一是要备份整个dir目录不只是dump.rdb或appendonly.aof因为 7.0 之后 AOF 是一个目录结构还带着 manifest 文件二是用cp -a保留时间戳和权限别用mvmv之后如果 Redis 被误启动它会在原路径重建空文件你连对比的样本都没了。# 先停掉可能自动拉起的进程 systemctl stop redis 2/dev/null ps -ef | grep redis-server | grep -v grep # 全目录备份保留属性 cp -a /var/lib/redis /var/lib/redis.bak.$(date %Y%m%d%H%M) # 看看文件大小和时间判断最后一次成功持久化是什么时候 ls -lh /var/lib/redis/ls -lh这一步信息量很大。如果dump.rdb的修改时间是三天前而 AOF 文件是几分钟前那说明 RDB 早就没成功过了很可能bgsave一直失败你只能依赖 AOF。反过来如果 AOF 文件时间是几小时前说明写入早就停了进程可能已经挂了很久那就要先去查为什么监控没告警。3.2 用 RDB 恢复先验证再加载RDB 恢复相对简单但也不是把文件一放就完事。第一步是用redis-check-rdb校验文件完整性这个工具会检查文件头、版本号、校验和如果rdbchecksum yes以及每条记录的编码。校验通过再加载能避免启动到一半失败又留下半截状态。redis-check-rdb /var/lib/redis/dump.rdb # 正常输出最后一行[OK] RDB checksum is valid如果校验报错先别急着扔。轻微的尾部损坏比如最后几条记录写了一半有时是可以截断的但 RDB 是二进制格式截断点很难手工找到实际操作中成功率不高。我的建议是RDB 文件损坏时优先找别的恢复源AOF、从节点、备份别在文件修复上耗太多时间。加载时有一个容易踩的坑dir和dbfilename必须指对。我习惯在启动前用redis-server --help之外的另一种方式确认——直接启动一个临时实例用CONFIG GET dir dbfilename看实际生效的值确认无误后再正式启动。redis-server /etc/redis/redis.conf --daemonize no # 启动日志里会有一行类似 # DB loaded from disk: 12.345 seconds这行日志很关键它告诉你加载耗时。如果 12GB 的数据加载花了 12 秒那下次做故障演练时就可以按这个比例估算恢复时间——大约 1GB/秒机械盘会慢很多SSD 上能到 1.5~2GB/秒。3.3 AOF 损坏的处理截断、修复与已知的取舍AOF 出问题的概率比 RDB 高得多因为它一直在追加写崩溃时尾部大概率是半条命令。好消息是Redis 默认配置aof-load-truncated yes遇到尾部不完整时会自动截断到最后一条完整命令并继续启动日志里会打印一段警告。这个默认行为救过很多人但前提是你得知道它存在——如果你把它设成了noRedis 会拒绝启动需要手工处理。比尾部截断更麻烦的是中间出现损坏。这种情况通常是磁盘坏块或者文件系统问题aof-load-truncated管不了启动会直接失败报Bad file format reading the append only file。这时候用redis-check-aof --fix# 先备份修复工具会原地修改文件 cp appendonly.aof appendonly.aof.orig redis-check-aof --fix appendonly.aof # 输出会告诉你截断了多少字节以及从哪一行开始--fix的逻辑是从头扫描遇到第一个不合法的命令就把后面全部截掉。也就是说如果损坏点在文件中间你会丢掉损坏点之后的所有数据即使那些数据是完好的。这个取舍必须提前想清楚是丢掉后半段但能启动还是保留文件另想办法。我的做法是两步走先--fix出一个能启动的版本让服务先跑起来同时保留原始文件然后把原始文件交给业务方做逐条解析把损坏点之后的关键命令捞出来用脚本补写回 Redis。这个补救过程听起来麻烦但比起丢几十分钟的订单数据值得。Redis 7.0 的多文件 AOF 下损坏通常局限在某个 incr 文件里处理粒度更细。先看appendonlydir/下的 manifest 文件内容确认哪些文件被引用再对单个文件做校验。这种情况下丢掉的范围比单文件模式小得多这也是升级到 7.0 的一个实际收益。3.4 恢复后的校验怎么确认数据真的回来了服务起来、端口通了不代表恢复成功。我见过加载了错误的 RDB 文件、数据整体回退了三天却没人发现的情况。所以恢复之后必须做校验不能只看PING返回PONG。校验分三层。第一层是数量级校验用DBSIZE和监控里的历史值对比偏差超过 10% 就要警惕。第二层是抽样校验从业务方拿几个已知的关键 key用TYPE和TTL看类型和过期时间是否正确——很多时候数据在但 TTL 丢了导致本该过期的缓存变成永久 key后面会引发内存问题。第三层是业务对账挑一个核心指标比如某个商品的总库存从 Redis 和数据库各算一遍比差值。redis-cli -n 0 DBSIZE redis-cli -n 0 INFO keyspace redis-cli -n 0 TTL order:status:20240513:8823 redis-cli -n 0 --scan --pattern sku:stock:* --count 1000 | head -20--scan而不是keys *这一点必须强调keys在大实例上会阻塞主线程恢复后本来就在预热阶段再来一次keys *很容易把服务打挂。这个坑我在早期项目里踩过一个 800 万 key 的实例上执行keys *主线程阻塞了将近 4 秒直接触发了一轮超时告警。4. 参数配置与容量估算把丢失窗口算清楚配置这件事最怕的是“抄了一份看起来合理的模板但不知道它在保护什么”。下面这几项参数直接决定了崩溃时你能救回多少我逐项说明它影响什么、怎么定值。4.1 关键参数逐项过一遍# 持久化相关 appendonly yes appendfsync everysec aof-use-rdb-preamble yes aof-load-truncated yes auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 4gb # RDB 作为补充 save 900 1 save 300 10 save 60 10000 rdbcompression yes rdbchecksum yes # 重写期间的保护 no-appendfsync-on-rewrite noauto-aof-rewrite-min-size这个值很多人设得很小比如 64mb结果就是频繁重写。重写期间 fork 加 IO 压力叠加容易出问题。我的经验是按实例内存的 1~2 倍来设写入量大的实例可以到 4GB 以上让重写频率降到一天几次而不是一小时几次。no-appendfsync-on-rewrite这个参数名字看着绕意思是“重写期间不做 fsync”。设成yes能减少重写期间主线程的阻塞代价是这段时间的写入丢失风险上升。默认no是更安全的选择除非你的磁盘 IO 扛不住。还有几个和恢复间接相关、但经常被忽略的系统层配置vm.overcommit_memory 1不设的话BGSAVEfork 时可能因为内存不足直接失败日志报Cant save in background: fork: Cannot allocate memory。这个错误在内存使用率高的实例上很常见。关闭透明大页THPTHP 会放大 COW 的内存开销fork 之后内存占用可能翻倍导致子进程被 OOM。maxmemory-policy要设对如果用的是allkeys-lru恢复后内存不够时 Redis 会主动淘汰 key你辛苦恢复的数据可能刚起来就被淘汰一批。恢复阶段建议临时改成noeviction等确认数据完整、内存调够了再改回去。4.2 一个具体的容量与恢复时长估算假设一个典型场景实例内存 16GB实际数据 12GB峰值写入 1.5 万条命令/秒平均命令大小 200 字节。AOF 增长量1.5 万 × 200 字节 ≈ 3MB/秒一小时约 10.8GB。这个量级意味着auto-aof-rewrite-min-size设 4GB 的话大约 20 分钟就会触发一次重写太频繁了。实际值应该按业务高峰持续时间调整或者用auto-aof-rewrite-percentage控制倍数。丢失窗口everysec策略下按 2 秒算1.5 万 × 2 3 万条命令约 6MB 数据。这部分就是最坏情况下要业务方补偿的量。恢复时长混合持久化下12GB 的 RDB 部分按 1.5GB/秒加载约 8 秒加上重写后残留的增量命令重放总体在 15 秒以内。纯文本 AOF 的话同样数据量可能要到 2~3 分钟。这个差距在故障恢复时是致命的服务不可用的每一秒都是损失。磁盘空间RDB 压缩后大约是内存的 40%~60%12GB 数据大约 5~7GB。AOF 在重写前可能涨到几十 GB磁盘预留至少要是数据量的 3 倍。我见过磁盘写满导致aof_last_write_status变err、然后 Redis 拒绝所有写请求的事故根因就是没留够重写空间。5. 高频故障排查与避坑清单恢复过程中遇到的问题八成集中在几个固定位置。这一节我把踩过的和见过的整理成速查表再加上几条文档里不会写的经验。5.1 常见问题速查表现象大概率原因排查动作启动报Short read or OOM loading DBRDB 文件不完整多半是磁盘满或进程被杀时正在写redis-check-rdb校验找备份或从节点启动报Bad file format reading the append only fileAOF 中间损坏redis-check-aof --fix先备份原文件启动后DBSIZE为 0dir/dbfilename配错或appendonly与实际文件不匹配CONFIG GET dir dbfilename appendonly逐项确认BGSAVE一直失败vm.overcommit_memory未设或磁盘空间不足查日志fork: Cannot allocate memory查df -h恢复后大量 key 瞬间消失maxmemory-policy触发了淘汰临时改noeviction或扩容内存恢复后 key 永不过期TTL 信息在恢复过程中丢失多见于手工补数据抽样TTL检查用脚本批量修正主从切换后数据比预期少异步复制导致未同步写入丢失对比master_repl_offset从业务日志补数据恢复后 CPU 打满缓存空窗期请求全打到数据库或大量 key 同时过期检查是否雪崩加随机 TTL 和限流5.2 几条真正花过代价换来的经验第一条恢复演练必须真做不能只写在文档里。我在一个项目里做过统计纸面演练过的恢复流程真出事时的成功率不到三成。原因很简单文档里写“加载 RDB 文件”实际执行时你会发现配置文件在另一台机器上、SSH 密钥过期了、备份目录权限不对。这些小事在凌晨三点会把人逼疯。建议每个季度做一次真实演练从备份恢复到一台独立实例记录每一步耗时把命令写成脚本。第二条备份要离线且要和持久化文件分开。持久化文件是“崩溃后恢复”备份是“持久化文件本身坏了怎么办”。这两者解决的是不同问题很多人只做了前者。我见过磁盘阵列整体损坏、持久化文件全没的情况最后靠三天前的离线备份救了命。备份频率取决于业务能接受丢多久不是取决于磁盘够不够。第三条AOF 尾部截断不要默认接受。aof-load-truncated yes让 Redis 能起来但你应该去查为什么尾部会不完整。常见的根因是磁盘 IO 抖动导致写入中断或者进程被强杀。如果不查清楚这事会反复发生每次都丢一点数据积少成多。第四条主从架构下恢复的优先级是先保从节点。主节点崩溃后如果从节点数据更新offset 更靠前别急着修主节点先把从节点提上来提供服务再去慢慢处理主节点。反过来的顺序会让服务多停很久而且主节点修复过程中如果被误拉起还可能触发全量同步把从节点也弄坏。第五条给恢复过程本身留一份操作日志。每次恢复都记录几点几分执行了什么命令、当时的DBSIZE是多少、加载耗时多少、最后丢了哪些数据。这份日志攒几次之后你就能算出自己这套架构的真实 RPO 和 RTO而不是拍脑袋估的。我在一个项目里就是这么发现实际 RTO 是文档里写的三倍的然后针对性地把 RDB 加载改成了从只读副本预热把不可用时间压了下来。第六条恢复后别急着开全量流量。数据刚加载完内存里是冷数据CPU 缓存的命中率、连接池、预热都没到位。直接放全量流量进来很容易出现响应时间飙高然后触发新一轮超时。我的习惯是先放 10% 流量跑五分钟看INFO stats里的keyspace_hits、evicted_keys和命令耗时稳定了再逐步放开。最后分享一个我自己一直在用的小办法把恢复流程的关键命令写成带注释的 shell 脚本脚本开头先强制做备份、强制打印当前INFO persistence和INFO keyspace的输出。人在紧张状态下容易跳步骤脚本能帮你把该做的事按顺序走完而且它本身就是最好的文档——比任何 wiki 页面都可靠因为它是真的被执行过的。