第 15 章
持久化选型与灾难恢复
第15章 持久化选型与灾难恢复
15.1 数据安全等级矩阵
在选择持久化方案之前,必须先回答一个根本性问题:你能接受损失多少数据?
| 持久化方案 | 最大丢失数据 | 恢复速度 | 文件大小 | CPU开销 | 内存开销 |
|---|---|---|---|---|---|
| 不持久化 | 全部(重启即清空) | N/A | 0 | 最低 | 最低 |
| RDB only(每小时) | 最多1小时 | 最快(5-10分钟/10GB) | 最小 | 低(fork) | 低 |
| RDB only(每5分钟) | 最多5分钟 | 最快 | 最小 | 中(频繁fork) | 中 |
| AOF everysec | ~1秒 | 慢(重放全部命令) | 大(增长无界) | 中 | 中 |
| AOF always | 最多1条命令 | 最慢 | 最大 | 高(fsync阻塞) | 中 |
| 混合持久化(推荐) | ~1秒 | 快(RDB+少量AOF重放) | 中 | 中 | 中 |
| RDB + AOF + 主从复制 | 接近0 | 最快(从库直接提升) | 中 | 较高 | 较高 |
15.2 各场景选型指南
15.2.1 场景1:纯缓存(允许全部丢失)
# 配置
save "" # 关闭 RDB
appendonly no # 关闭 AOF
# 适用场景:
# - Session 存储(用户可以重新登录)
# - 页面/API 响应缓存
# - 临时计算中间结果
# - CDN 热数据预取
优势:零持久化开销,最大吞吐量,无 fork() 延迟。
注意:关闭持久化后,主从复制仍然有效,从库可以作为内存备份(但也是内存数据,主库崩溃后从库不会自动提升为主库)。
15.2.2 场景2:缓存 + 快速重建(可接受分钟级数据丢失)
# 配置
save 3600 1
save 300 100
save 60 10000
appendonly no
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /var/lib/redis
# 适用场景:
# - 商品信息缓存(数据可从数据库重建)
# - 排行榜(允许短暂回退)
# - 实时统计计数器(允许少量误差)
重建时间估算(10GB RDB):
磁盘读取速度:500MB/s(NVMe SSD)
读取时间:10GB / 500 = 20秒
RDB 解析+写入内存:~40秒
总恢复时间:约 1 分钟
从数据库重建(SQL查询):可能需要数小时
15.2.3 场景3:业务数据(不允许丢失超过1秒)
# 配置(混合持久化,推荐作为生产默认值)
save 3600 1
save 300 100
save 60 10000
appendonly yes
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-use-rdb-preamble yes
# 适用场景:
# - 用户积分/余额(丢失1秒可接受)
# - 订单状态缓存(非支付核心链路)
# - 消息队列(Stream)
# - 实时库存(有补偿机制)
15.2.4 场景4:金融/订单(接近零丢失)
# 配置
appendonly yes
appendfsync always # 每条命令强制 fsync
aof-use-rdb-preamble yes
save "" # 可关闭 RDB,或保留作为备份
# 同时配置主从复制(双重保障)
# 主库写成功 + 至少1个从库确认 → 才返回客户端
wait 1 100 # 等待至少1个从库同步,超时100ms
# 适用场景:
# - 支付流水
# - 金融账户余额(Redis作为缓存层,仍需配合持久化DB)
# - 抢购/秒杀库存(高一致性要求)
注意:appendfsync always + WAIT 1 100 的组合将写入 TPS 降低至约 5,000-15,000,但提供了接近双写的数据保障。
15.3 生产备份策略
15.3.1 分层备份架构
Level 1(本地实时):AOF 文件
→ 任何时候崩溃,最多丢失 1 秒(everysec)
Level 2(本地每日):RDB 快照
→ 每天凌晨 3 点触发 BGSAVE,保留 7 天
Level 3(远程对象存储):
→ 每小时上传最新 RDB 到 S3/OSS,保留 30 天
Level 4(跨机房从库):
→ 实时同步,RPO ≈ 毫秒级(网络延迟)
15.3.2 备份脚本
#!/bin/bash
# /etc/cron.d/redis-backup
# 每小时整点执行
set -euo pipefail
REDIS_CLI="/usr/bin/redis-cli"
REDIS_DIR="/var/lib/redis"
BACKUP_DIR="/backup/redis"
S3_BUCKET="s3://company-redis-backup"
RETAIN_LOCAL_DAYS=7
RETAIN_REMOTE_DAYS=30
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
timestamp=$(date +%Y%m%d_%H%M%S)
# 1. 触发 BGSAVE
log "Triggering BGSAVE..."
$REDIS_CLI BGSAVE
# 2. 等待 BGSAVE 完成(最多等 5 分钟)
for i in $(seq 1 60); do
status=$($REDIS_CLI LASTSAVE)
in_progress=$($REDIS_CLI INFO persistence | grep rdb_bgsave_in_progress | cut -d: -f2 | tr -d '\r')
if [ "$in_progress" = "0" ]; then
log "BGSAVE completed."
break
fi
[ $i -eq 60 ] && { log "ERROR: BGSAVE timeout!"; exit 1; }
sleep 5
done
# 3. 复制 RDB 到本地备份目录
mkdir -p "$BACKUP_DIR"
cp "$REDIS_DIR/dump.rdb" "$BACKUP_DIR/dump_${timestamp}.rdb"
log "Local backup: $BACKUP_DIR/dump_${timestamp}.rdb ($(du -sh "$BACKUP_DIR/dump_${timestamp}.rdb" | cut -f1))"
# 4. 上传到 S3
log "Uploading to S3..."
aws s3 cp "$BACKUP_DIR/dump_${timestamp}.rdb" \
"$S3_BUCKET/$(hostname)/dump_${timestamp}.rdb" \
--storage-class STANDARD_IA
log "S3 upload completed."
# 5. 清理本地旧备份
find "$BACKUP_DIR" -name "dump_*.rdb" -mtime "+${RETAIN_LOCAL_DAYS}" -delete
log "Local backups older than ${RETAIN_LOCAL_DAYS} days deleted."
# 6. 清理远程旧备份
aws s3 ls "$S3_BUCKET/$(hostname)/" \
| awk '{print $4}' \
| while read file; do
file_date=$(echo "$file" | grep -oP '\d{8}' | head -1)
if [ -n "$file_date" ]; then
age=$(( ($(date +%s) - $(date -d "$file_date" +%s)) / 86400 ))
if [ $age -gt $RETAIN_REMOTE_DAYS ]; then
aws s3 rm "$S3_BUCKET/$(hostname)/$file"
log "Deleted remote backup: $file (${age} days old)"
fi
fi
done
log "Backup completed successfully."
15.3.3 备份验证(每周)
#!/bin/bash
# 验证 RDB 文件完整性
verify_rdb() {
local file=$1
redis-check-rdb "$file" 2>&1
if [ $? -eq 0 ]; then
echo "OK: $file"
else
echo "CORRUPTED: $file"
# 发送告警
curl -X POST "$ALERT_WEBHOOK" -d "{\"text\": \"Redis backup corrupted: $file\"}"
fi
}
# 验证最近的本地备份
latest=$(ls -t /backup/redis/dump_*.rdb | head -1)
verify_rdb "$latest"
# 从 S3 下载并验证(每周一次)
if [ "$(date +%u)" = "1" ]; then
aws s3 cp "$(aws s3 ls s3://company-redis-backup/$(hostname)/ \
| sort | tail -1 | awk '{print $4}')" /tmp/verify.rdb
verify_rdb /tmp/verify.rdb
rm /tmp/verify.rdb
fi
15.4 灾难恢复演练
15.4.1 场景1:Redis 进程崩溃,数据在磁盘
症状:redis-server 进程消失,客户端连接报错。
处理步骤:
# 1. 确认磁盘上有 RDB/AOF 文件
ls -la /var/lib/redis/
# dump.rdb appendonly.aof appendonlydir/
# 2. 检查文件完整性
redis-check-rdb /var/lib/redis/dump.rdb
redis-check-aof /var/lib/redis/appendonly.aof
# 3. 直接重启(Redis 自动加载)
systemctl start redis
# 4. 验证数据
redis-cli INFO keyspace
redis-cli DBSIZE
# 比对恢复前的预期值
# 恢复时间估算(10GB 混合持久化):
# RDB 加载:~40秒
# AOF 增量重放(通常<1秒的增量):~1秒
# 总计:约 1 分钟
15.4.2 场景2:磁盘损坏,从备份恢复
症状:磁盘故障,数据文件丢失。
处理步骤:
# 1. 停止 Redis(如果还在运行)
systemctl stop redis
# 2. 挂载新磁盘或修复磁盘
# 3. 从 S3 下载最近的 RDB 备份
aws s3 ls s3://company-redis-backup/$(hostname)/ | sort | tail -5
# 选择合适的时间点
aws s3 cp s3://company-redis-backup/$(hostname)/dump_20240101_030000.rdb \
/var/lib/redis/dump.rdb
# 4. 设置正确的文件权限
chown redis:redis /var/lib/redis/dump.rdb
chmod 644 /var/lib/redis/dump.rdb
# 5. 如果有对应时间点之后的 AOF 增量(理想情况):
# - 确认 AOF 文件有效
# - 放置到正确位置
# 6. 验证并启动
redis-check-rdb /var/lib/redis/dump.rdb
systemctl start redis
redis-cli PING # 应返回 PONG
# 7. 数据核对(与应用层或日志比对丢失量)
redis-cli INFO keyspace
redis-cli DBSIZE
RTO(恢复时间目标)计算:
下载 10GB RDB(100Mbps 带宽):~14 分钟
文件复制 + 启动 Redis:~1 分钟
RDB 加载:~1 分钟
总 RTO:约 16 分钟(带宽是瓶颈)
优化:使用 S3 Transfer Acceleration 或预置备用实例
15.4.3 场景3:误执行 FLUSHALL
症状:所有数据被清空,DBSIZE 返回 0。
紧急处理(使用 AOF 恢复):
# 立即停止 Redis(防止 AOF rewrite 覆盖掉 FLUSHALL 之前的数据)
redis-cli SHUTDOWN NOSAVE
# 注意:NOSAVE 避免保存空的 RDB,否则 dump.rdb 也会被清空
# 1. 备份当前的 AOF 文件
cp /var/lib/redis/appendonly.aof /tmp/appendonly.aof.bak
# 2. 在 AOF 中定位并删除 FLUSHALL 命令
python3 << 'EOF'
import re
with open('/var/lib/redis/appendonly.aof', 'rb') as f:
content = f.read()
# RESP 格式的 FLUSHALL:*1\r\n$8\r\nFLUSHALL\r\n
# 或者 *2\r\n$8\r\nFLUSHALL\r\n$5\r\nASYNC\r\n
pattern = re.compile(
rb'\*\d+\r\n(?:\$\d+\r\n(?:(?!FLUSHALL)[^\r]*)\r\n)*\$8\r\nFLUSHALL\r\n(?:\$\d+\r\n[^\r]*\r\n)?',
re.IGNORECASE
)
matches = pattern.findall(content)
print(f"Found {len(matches)} FLUSHALL command(s)")
for m in matches:
print(f" Match: {m[:50]}...")
cleaned = pattern.sub(b'', content)
print(f"Original: {len(content)} bytes → Cleaned: {len(cleaned)} bytes")
with open('/var/lib/redis/appendonly.aof', 'wb') as f:
f.write(cleaned)
print("Done.")
EOF
# 3. 验证修改后的 AOF
redis-check-aof /var/lib/redis/appendonly.aof
# 4. 禁用 RDB(避免加载时被空 dump.rdb 覆盖,如果 dump.rdb 已损坏)
# 临时修改配置:注释掉 save 行,或删除空 dump.rdb
rm /var/lib/redis/dump.rdb # 如果 dump.rdb 是 FLUSHALL 后保存的空文件
# 5. 重启 Redis 加载修复后的 AOF
redis-server /etc/redis/redis.conf
# 6. 核对恢复结果
redis-cli DBSIZE
redis-cli RANDOMKEY
15.4.4 场景4:数据错误,回滚到某时间点
场景:应用代码 bug 在 14:00 写入了大量错误数据,需要回滚到 13:55 的状态。
# 1. 找到 13:55 时间点对应的 RDB 备份
ls /backup/redis/ | grep "20240101_13"
# dump_20240101_130000.rdb (13:00)
# dump_20240101_140000.rdb (14:00,已含错误数据)
# 最近的正确备份是 13:00 的
# 2. 从 AOF 中提取 13:00-13:55 之间的命令
# (Redis 7.0 的 aof-timestamp-enabled 可以精确定位)
# 如果没有时间戳,只能从 13:00 RDB 开始,手动核对数据
# 3. 在测试环境验证恢复结果
# 临时启动一个 Redis 实例加载 13:00 的 RDB
redis-server --port 6380 --dbfilename dump_20240101_130000.rdb \
--dir /backup/redis --daemonize yes
# 4. 在测试实例上重放 13:00-13:55 的命令(需要手动从 AOF 提取)
redis-cli -p 6380 DBSIZE # 验证恢复结果
# 5. 满意后,停主实例,替换数据文件,重启
redis-cli SHUTDOWN NOSAVE
cp /backup/redis/dump_20240101_130000.rdb /var/lib/redis/dump.rdb
# 删除 AOF(或手动截断至 13:55)
redis-server /etc/redis/redis.conf
15.5 redis-check-rdb 和 redis-check-aof 工具
15.5.1 redis-check-rdb
# 基本用法
redis-check-rdb dump.rdb
# 详细输出(解析每个 key)
redis-check-rdb --dump dump.rdb 2>&1 | head -100
# 常见错误和含义
# "Wrong RDB checksum"
# → 文件末尾8字节校验和不匹配,文件可能损坏或被截断
# → 原因:BGSAVE 期间磁盘写满/断电/文件系统错误
# "RDB version N is not supported" (N > current Redis RDB version)
# → RDB 文件来自更新版本的 Redis,当前版本无法读取
# → 解决:升级 Redis 版本
# "DB load failed"
# → 文件在某个 key 解析处失败,文件部分损坏
# → 此时只能从更早的备份恢复
15.5.2 redis-check-aof
# 检查 AOF 完整性
redis-check-aof appendonly.aof
# 自动修复(截断到最后完整命令)
redis-check-aof --fix appendonly.aof
# "Successfully truncated AOF appendonly.aof to offset 2048"
# 注意:被截断的部分命令永久丢失
# 常见问题
# "AOF is not valid":通常是写入中途崩溃导致的末尾不完整命令
# → --fix 可以安全修复
# "Bad file format":非 RESP 格式或文件头损坏
# → 如果是混合 AOF,确认 RDB 部分是否完整
15.6 主从 + 持久化的最佳组合
15.6.1 常见的主从持久化配置
配置A:主库轻量,从库保障(推荐生产)
# 主库(master)
save "" # 关闭 RDB(减少 fork 延迟)
appendonly yes # 开启 AOF
appendfsync everysec
# 从库(replica)
save 3600 1 # 开启 RDB(定期快照)
save 300 100
appendonly yes # 开启 AOF
appendfsync everysec
# 原因:
# 主库不做 BGSAVE,避免 fork() 阻塞影响写入延迟
# 从库承担持久化开销,主库崩溃时从库有完整数据
配置B:主库也持久化(数据最安全)
# 主库
save 300 100
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes
# 从库(延迟从库,30秒延迟,用于防误操作)
repl-backlog-size 100mb
# 配置 replica-lazy-flush yes 避免全量同步时阻塞
15.6.2 哨兵(Sentinel)与持久化
# sentinel.conf 中配置:
sentinel down-after-milliseconds mymaster 5000 # 5秒未响应则认为故障
sentinel failover-timeout mymaster 10000 # 10秒故障转移超时
sentinel parallel-syncs mymaster 1 # 同时只同步1个从库
# 持久化注意事项:
# 主库切换后,新主库可能因 RDB 加载还未完成而导致短暂数据缺失
# 建议:failover 前确认新主库的 info replication 状态 = master
# 以及 rdb_bgsave_in_progress = 0
15.6.3 关闭持久化时主从复制的风险
# 危险场景:
# 主库关闭持久化,从库是 replica
# 主库崩溃后重启 → 数据为空(无持久化)
# 主库重启后,从库检测到主库重启,触发全量同步
# 从库用空数据覆盖自己 → 所有数据丢失!
# 解决方案:
# 方案1:主库即使关闭持久化,也要确保崩溃后不会自动重启
# (通过 systemd 配置 Restart=no 或 on-failure)
# 方案2:主库开启持久化(接受少量延迟开销)
# 方案3:主库崩溃后,先手动提升从库为主库,再重启原主库作为新从库
15.7 持久化监控告警体系
# 基于 Redis INFO 的监控检查脚本
import redis
import time
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
def check_persistence_health():
info = r.info('persistence')
alerts = []
# 1. RDB 最后保存时间(超过1小时告警)
last_save = info['rdb_last_save_time']
if time.time() - last_save > 3600:
alerts.append(f"WARNING: No RDB save in {(time.time()-last_save)/3600:.1f} hours")
# 2. RDB 保存失败
if info['rdb_last_bgsave_status'] != 'ok':
alerts.append(f"CRITICAL: Last BGSAVE failed: {info['rdb_last_bgsave_status']}")
# 3. AOF 写入失败
if info.get('aof_last_write_status') == 'err':
alerts.append("CRITICAL: AOF write failed - writes being rejected!")
# 4. AOF Rewrite 超时(超过10分钟)
if info.get('aof_rewrite_in_progress') == 1:
rewrite_time = info.get('aof_current_rewrite_time_sec', 0)
if rewrite_time > 600:
alerts.append(f"WARNING: AOF rewrite running for {rewrite_time}s")
# 5. COW 内存使用过大(超过 1GB)
rdb_cow = info.get('rdb_last_cow_size', 0)
if rdb_cow > 1024 * 1024 * 1024:
alerts.append(f"WARNING: Large RDB COW: {rdb_cow/1024/1024:.0f} MB")
# 6. AOF pending fsync 积压
pending = info.get('aof_pending_bio_fsync', 0)
if pending > 1000:
alerts.append(f"WARNING: {pending} fsync operations pending")
return alerts
alerts = check_persistence_health()
for a in alerts:
print(a)
15.8 持久化决策树
开始
│
▼
能否接受重启后数据全部丢失?
├─ YES → 关闭 RDB 和 AOF(纯内存,最高性能)
│
└─ NO
│
▼
能否接受丢失分钟级数据?
├─ YES → RDB only(选择合适的 save 间隔)
│
└─ NO
│
▼
能否接受丢失约1秒数据?
├─ YES → 混合持久化(aof-use-rdb-preamble yes + everysec)
│ 这是大多数业务场景的推荐选项
│
└─ NO
│
▼
接近零丢失要求?
└─ YES → AOF always + WAIT 1 N(至少等1个从库确认)
注意:TPS 降低至 5,000-15,000