第 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
本章评分
4.8  / 5  (20 评分)

💬 留言讨论