外观
从误删全库到半小时恢复:Redis RDB回滚+AOF重放实战
问题背景:一条 FLUSHALL 触发的恢复演练
凌晨的告警群里,最让人头皮发麻的日志莫过于 FLUSHALL 被误执行。无论来源是脚本连错环境,还是运维同学在生产库上手动敲了清理命令,结果都一样:内存数据瞬间清空,如果业务把 Redis 当作主存储(订单流水、会话、分布式锁、排行榜),损失会被迅速放大。
缓存可丢的数据,靠数据库回源尚能续命;但一旦涉及 Hash 结构的账户余额、ZSet 排行榜、Stream 消息队列,没有持久化文件就等于永久丢失。因此,高可用体系里不能只盯着主从切换和哨兵,数据恢复(Recovery) 才是最后一道防线,而衡量它的核心指标就是 RTO(Recovery Time Objective,恢复时间目标) 与 RPO(数据丢失窗口)。
本文记录一次受控的应急演练:在测试环境灌入真实量级数据后模拟误删,使用「RDB 基线回滚 + AOF 增量重放」方案,目标是把 RTO 控制在 30 分钟内,RPO 压缩到秒级。
恢复方案对比:为什么选 RDB + AOF 组合
Redis 持久化主要有 RDB 快照与 AOF 日志两条路线,单独使用都有明显短板。
三种典型策略的取舍如下:
- 纯 RDB:文件紧凑、恢复速度快,适合批量回滚;但快照是周期性的,两次快照之间的写入会丢,RPO 可能达到分钟级。
- 纯 AOF(appendfsync always/everysec):每条或每秒写命令都落盘,RPO 极低;但文件膨胀快,全量重放一个几十 GB 的 AOF 往往要数十分钟,RTO 难以保证。
- RDB + AOF 混合(推荐):Redis 4.0 起开启
aof-use-rdb-preamble,AOF 重写时前半段是 RDB 二进制基线,后半段是增量命令。恢复时先快速加载基线,再重放少量增量,兼顾 RTO 与 RPO。
关键认知是:误删操作本身也会被 AOF 忠实记录。直接拿当前 AOF 重启,FLUSHALL 会被再次执行,恢复等于白做。所以必须先截断 AOF,把灾难命令及其之后的内容剔除,再配合最近一个干净 RDB 做基线。
演练环境准备与数据灌入
先用 Docker 起一个开启混合持久化的 Redis 7 实例,目录独立挂载,便于操作文件:
bash
# 启动开启 AOF + 混合持久化的 Redis
mkdir -p /data/redis-drill && cd /data/redis-drill
docker run -d --name redis-drill --network host \
-v /data/redis-drill:/data redis:7.0 \
redis-server --appendonly yes \
--aof-use-rdb-preamble yes \
--appendfsync everysec \
--save "60 100"灌入几种典型数据结构,模拟账户、排行榜与会话:
bash
# Hash:账户余额(关键主存数据)
redis-cli HSET account:1001 name alice balance 8800 level vip
redis-cli HSET account:1002 name bob balance 1200 level normal
# ZSet:消费排行榜
redis-cli ZADD rank:pay 99 alice 158 bob 305 carol
# String:登录会话,带过期时间
redis-cli SET session:token-alice "uid=1001" EX 3600
# Stream:订单事件(消费者组)
redis-cli XADD order:stream '*' uid 1001 amount 199 action buy
redis-cli XGROUP CREATE order:stream cg1 0 MKSTREAM随后手动触发一次快照与 AOF 重写,制造一个「干净基线」,并把备份目录固化下来:
bash
redis-cli BGSAVE # 生成 dump.rdb
redis-cli BGREWRITEAOF # 生成 RDB 前缀的混合 AOF
# 等待后台任务完成
sleep 5
cp /data/redis-drill/dump.rdb /data/redis-drill/dump.clean.rdb
cp -r /data/redis-drill/appendonlydir /data/redis-drill/aof.backup模拟误删并定位故障点
再写入一批基线之后的增量数据,然后执行误删:
bash
# 基线之后的正常增量写入(期望被找回)
redis-cli HSET account:1003 name carol balance 5600 level vip
redis-cli ZINCRBY rank:pay 50 carol
redis-cli SET session:token-bob "uid=1002" EX 1800
# 灾难命令
redis-cli FLUSHALL
redis-cli DBSIZE # 返回 0,确认已清空第一步永远是隔离,而不是急着重启。 立刻停止业务写入、下线实例或切断客户端连接,防止 AOF 继续追加、覆盖证据。若 Redis 仍在运行,先做现场留存:
bash
# 记录故障时间点与最后写入,冻结现场
redis-cli LASTSAVE
date +%s
cp -r /data/redis-drill/appendonlydir /data/redis-drill/aof.crime.scene
cp /data/redis-drill/dump.rdb /data/redis-drill/dump.crime.rdbAOF 在 Redis 7 中位于 appendonlydir 目录,增量部分文件名类似 appendonly.aof.1.incr.aof。用 redis-check-aof 可以在截断前查看其结构,误删命令以明文 RESP 协议存在:
bash
# 查看 AOF 中明文命令(能看到 FLUSHALL 记录)
cat /data/redis-drill/appendonlydir/appendonly.aof.*.incr.aof | strings | grep -n FLUSHALLRDB 回滚 + AOF 截断重放实操
第一步:用干净 RDB 建立基线
停掉实例,把故障文件全部移走,换成误删前的干净快照:
bash
docker stop redis-drill
cd /data/redis-drill
rm -f dump.rdb
cp dump.clean.rdb dump.rdb
# 暂时移走现有 AOF,避免重启时直接重放到 FLUSHALL
mv appendonlydir appendonlydir.crime为了让基线之后的增量能够补回,我们不直接用混合 AOF 整体恢复,而是先让 Redis 仅基于 RDB 启动,再单独处理增量 AOF。更稳妥的做法是使用 redis-check-aof --truncate 截断到损坏点,但 FLUSHALL 并非「损坏」,工具不会自动识别,需要手工定位字节偏移。
第二步:精确截断 AOF,剔除灾难命令
在备份的增量 AOF 中找到 FLUSHALL 起始位置。AOF 是 RESP 协议文本,一条命令形如 *1\r\n$8\r\nFLUSHALL\r\n。用脚本定位该命令的字节偏移并截断:
bash
# 在演练副本上操作,绝不直接改原始证据
cp -r aof.crime.scene appendonlydir
INCR=$(ls appendonlydir/appendonly.aof.*.incr.aof | head -1)
# 用 python 精确定位 FLUSHALL 这条 RESP 命令的起点(*1 头)
python3 - <<'PY'
import re, glob
f = glob.glob('/data/redis-drill/appendonlydir/appendonly.aof.*.incr.aof')[0]
data = open(f,'rb').read()
# 匹配整条命令:*1..$8..FLUSHALL..
pat = re.compile(rb'\*1
\$8
FLUSHALL
')
m = pat.search(data)
assert m, '未找到 FLUSHALL'
print('截断偏移:', m.start())
open(f,'wb').write(data[:m.start()]) # 保留误删之前的全部增量
print('截断后大小:', m.start())
PY截断后务必校验文件完整性,避免截在半条命令中间导致加载失败:
bash
redis-check-aof --fix "$INCR" || true
# 确认 FLUSHALL 已不存在
strings "$INCR" | grep -c FLUSHALL # 期望输出 0注意:如果误操作是 KEYS 后批量 DEL、或业务侧 unlink 了大量 key,需要截断到第一条删除命令之前,宁可多丢一点后续写入,也不能保留任何删除指令。
第三步:重启加载并追平增量
将处理好的 AOF 与干净 RDB 放回,重启实例。混合持久化下,BASE 类型的 AOF(RDB 前缀)加载很快,INCR 部分只重放截断后保留的少量命令:
bash
docker start redis-drill
sleep 3
redis-cli CONFIG GET aof-use-rdb-preamble第四步:数据结构校验
逐类核对恢复结果,重点确认「基线数据」与「误删前增量」都在:
bash
redis-cli DBSIZE # 应恢复到误删前的 key 数
redis-cli HGETALL account:1001 # 基线 Hash
redis-cli HGETALL account:1003 # 增量 Hash,name=carol
redis-cli ZSCORE rank:pay carol # 增量 ZINCRBY 后应为 355
redis-cli GET session:token-bob # 增量会话
redis-cli XLEN order:stream # Stream 消息条数
redis-cli EXISTS session:token-alice # 带 TTL 的 key 仍在
redis-cli TTL session:token-bob # TTL 被重放后会重新计时本次演练各阶段耗时:隔离与现场冻结约 3 分钟,定位快照与拷贝约 4 分钟,RDB 基线启动约 6 分钟,AOF 定位截断约 5 分钟,重放与校验约 8 分钟,切流约 2 分钟,整体 RTO 约 28 分钟,达到半小时目标;RPO 仅为截断点到故障之间约 1 秒的写入(everysec 策略固有)。
避坑总结:实战中踩过的坑
直接拿当前 AOF 重启,FLUSHALL 被重放第二次。 现象是日志显示加载成功但
DBSIZE仍为 0。根因是 AOF 完整记录了误删命令。修正方法:恢复前必须在副本上截断 AOF,剔除FLUSHALL/FLUSHDB/批量DEL及其之后的内容,再用干净 RDB 做基线,绝不在原始文件上直接改。在运行中的实例上执行 BGREWRITEAOF 破坏证据。 误删后若手滑触发重写,Redis 可能用当前空内存重写出一个「干净但为空」的 AOF,把含历史增量的旧文件覆盖轮转掉,数据再难找回。正确顺序是先
docker stop或切断写入、完整cp -r冻结appendonlydir与dump.rdb,所有修复都在副本上进行。截断位置算错,切在 RESP 命令中间导致加载报错。 典型报错为
Bad file format reading the append only file。要按*<argc>\r\n$<len>\r\n的命令边界定位起点,而不是简单搜索字符串FLUSHALL;截断后必须跑redis-check-aof,并确认目标命令计数为 0。忽略 TTL 与 Stream 消费组的语义偏差。 AOF 重放相对时间命令(如
SET EX)会以恢复时刻重新计算过期,Stream 消费者组的XACK、待处理条目(PEL)状态也可能与故障瞬间不完全一致。恢复后不能只看DBSIZE,要对关键 Hash 余额、ZSet 分数、TTL、消费组进度做业务级抽样比对。把 RTO 等同于文件加载时间。 真正的 RTO 包含发现告警、权限审批、找备份、拷贝文件、校验和切流。建议把「快照异地备份、一键冻结脚本、恢复 Runbook、定期演练」纳入高可用体系,并把
appendfsync everysec与aof-use-rdb-preamble yes设为生产默认,才能在真实故障中稳定复现半小时恢复。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。