第22章:Redis 持久化 RDB 与 AOF:数据不丢的底线

1. 项目背景

1.1 业务场景

订单团队逐步把 Redis 用在了越来越多的场景------秒杀库存预热、活动资格缓存、验证码、排行榜、会话状态。刚开始大家都说"Redis 是缓存,丢了可以回源"。但随着业务演进,Redis 里存的数据越来越多地变成了"不能随便丢"的数据:

  • 秒杀库存状态:Redis 中 DECR 到 50 件,如果重启后变回 100,就会超卖。
  • 排行榜积分:用户花了一小时打的积分,如果 Redis 重启丢了,运营活动无法复盘。
  • 签到数据:用户完成 7 天连续签到,如果 Redis 重启后签到记录消失,用户投诉。

更微妙的是------即使数据"最终可以从数据库恢复",恢复过程也需要时间。如果 Redis 在流量高峰期宕机,全量回源会对数据库造成雪崩式压力。持久化不仅是"数据不丢",也是"快速恢复服务"。

1.2 RDB vs AOF 一句话理解

方案 比喻 原理 优势 劣势
RDB 定期拍照片 把内存数据快照写入磁盘 恢复快、文件小、适合备份 两次快照间数据可能丢失
AOF 记账本 记录每条写命令到日志 丢失窗口小(1秒) 文件大、写放大、恢复慢
混合 照片+账本 AOF 前半段 RDB + 后半段增量 兼顾两者优势 格式可读性差

1.3 本章目标

  • 理解 RDB 的 BGSAVE/fork/写时复制机制及影响。
  • 掌握 AOF 的 appendfsync 三种策略及生产选择。
  • 实践混合持久化配置。
  • 完成数据恢复演练流程。
  • 理解持久化不只是"开不开"------更要回答四个问题:RPO(最多丢多少)、RTO(多久恢复)、磁盘 IO 预算、恢复演练。

2. 项目设计

2.1 第一幕:RDB 是拍照,不是录像

小胖 :"RDB 不就是给 Redis 拍张照片吗?BGSAVE 一下,数据全存到 dump.rdb 里,恢复的时候加载回来。"

小白:"那拍照的瞬间------如果 Redis 内存有 30GB,拍这张照片要多久?拍照期间新的写入怎么办?如果拍完照到下次拍照之间 Redis 挂了,中间的数据去哪了?"

大师 :"三个好问题。第一,BGSAVE 是后台快照------Redis 调用 fork() 创建子进程,子进程负责把内存数据写入磁盘。主进程继续处理命令。这也是 Redis 需要预留内存的原因------fork 期间内存会被写时复制(Copy-on-Write)。第二,子进程看到的是 fork 瞬间的内存快照------之后的写入不会体现在这份 RDB 中。第三,两次 RDB 之间的数据确实可能丢失。"

perl 复制代码
RDB 的时间线:

  10:00:00  BGSAVE (fork子进程)
  10:00:01  SET key1 "a"    ← 子进程看不到这条写入
  10:00:02  INCR counter    ← 子进程也看不到这条
  10:00:05  子进程完成 RDB (只有 10:00:00 之前的数据)
  10:00:10  Redis 宕机
  → 恢复: 10:00:00 的快照 → 丢失了 10:00:01~10:00:10 的数据

技术映射 :RDB 的保存间隔由 save 配置控制(如 save 900 1 = 900 秒内至少 1 个 key 变化则触发 BGSAVE)。间隔越短数据越安全但磁盘 IO 越多。

2.2 第二幕:AOF------每笔都记账,但记得累

小胖:"那 AOF 是不是就像会计记账------每次写操作都记一笔?这样最多丢 1 秒的数据?"

大师:"对,AOF 的核心是把每条写命令追加到日志文件。但它有个'刷盘策略'的选择------你多久刷一次磁盘?"

markdown 复制代码
appendfsync 三种策略:

  always:   每一条写命令都 fsync 到磁盘
            优点: 数据最安全(几乎不丢)
            缺点: 性能最差(磁盘 IO 成为瓶颈)
            适用: 极少数对数据安全要求极高的场景

  everysec: 每秒 fsync 一次
            优点: 最多丢 1 秒数据,性能好
            缺点: 极端情况(断电)可能丢 1 秒
            适用: 99% 的生产场景

  no:       由操作系统决定何时刷盘
            优点: 性能最好
            缺点: 丢多少数据不确定(通常 30 秒)
            适用: 纯缓存、数据可完全从数据库恢复

小白追问:"AOF 文件一直追加,会不会越来越大?比如一个 key 被 INCR 了 100 次,AOF 里就有 100 条记录。"

大师 :"所以有 AOF rewrite(重写)。Redis 会根据当前内存数据重新生成一份更短的 AOF------比如那 100 条 INCR 被压缩成一条 SET key 100。重写是后台子进程完成的,不影响主进程。"

技术映射 :auto-aof-rewrite-percentage 100 + auto-aof-rewrite-min-size 64mb 表示当 AOF 文件大小比上次重写后增长 100% 且超过 64MB 时自动触发重写。

2.3 第三幕:混合持久化------鱼和熊掌兼得

小胖:"RDB 恢复快但丢数据多,AOF 丢数据少但恢复慢------Redis 8.x 能不能两全其美?"

大师 :"这就是混合持久化。开启 aof-use-rdb-preamble yes 后,AOF 重写时文件的前半段是 RDB 格式(快速加载),后半段是 AOF 增量命令(补齐 RDB 之后的数据)。恢复时先加载 RDB 部分快速恢复大部分数据,再回放 AOF 尾部补齐。"

markdown 复制代码
混合持久化文件结构:

  ┌─────────────────────────────────┐
  │  RDB 格式(内存快照)             │  ← 体积小,加载快
  ├─────────────────────────────────┤
  │  AOF 格式(快照后的增量命令)      │  ← 补齐差量
  └─────────────────────────────────┘

  恢复流程:
    1. 先加载 RDB 部分 → 恢复大部分数据(秒级)
    2. 再回放 AOF 尾部 → 补齐 RDB 之后的写入(毫秒级)
    3. 总恢复时间 ≈ RDB 加载时间 + 增量回放时间

3. 项目实战

3.1 环境准备

bash 复制代码
mkdir -p redis-lab-22-data
docker run --name redis-lab-22 -p 6382:6379 \
  -v "$PWD/redis-lab-22-data:/data" \
  -d redis:8.6 redis-server --dir /data --appendonly yes
docker exec -it redis-lab-22 redis-cli

3.2 实战一:RDB 快照与恢复

bash 复制代码
# 写入业务数据
127.0.0.1:6379> SET stock:sku:1001 100
OK
127.0.0.1:6379> ZADD rank:activity:202607 80 user:1 95 user:2
(integer) 2
127.0.0.1:6379> HSET order:brief:1001 status pending amount 99
(integer) 2

# 手动触发后台快照
127.0.0.1:6379> BGSAVE
Background saving started

# 查看持久化状态
127.0.0.1:6379> INFO persistence
# rdb_last_bgsave_status:ok     ← 最近 RDB 是否成功
# rdb_changes_since_last_save:0 ← 上次保存以来的变更数

模拟崩溃恢复:

bash 复制代码
127.0.0.1:6379> SET newest:key "after-last-bgsave"
OK
docker kill redis-lab-22
docker start redis-lab-22
docker exec -it redis-lab-22 redis-cli
127.0.0.1:6379> GET stock:sku:1001   # ✓ 恢复了
"100"
127.0.0.1:6379> GET newest:key       # ✗ 丢失了
(nil)

3.3 实战二:AOF 配置与效果

bash 复制代码
127.0.0.1:6379> CONFIG SET appendonly yes
OK
127.0.0.1:6379> CONFIG SET appendfsync everysec
OK
127.0.0.1:6379> SET aof:test:key "aof-enabled"
OK
127.0.0.1:6379> BGREWRITEAOF
# 崩溃恢复后验证 AOF 数据保留了

3.4 实战三:混合持久化配置

conf 复制代码
# redis.conf 持久化部分(生产推荐配置)
save 900 1
save 300 10
save 60 10000
dbfilename dump.rdb
dir /data
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-use-rdb-preamble yes          # 混合持久化
min-replicas-to-write 1
min-replicas-max-lag 10

3.5 实战四:RDB 配置策略详解

bash 复制代码
127.0.0.1:6379> CONFIG GET save
# save 900 1   → 900秒内至少1次变更 → BGSAVE
# save 300 10  → 300秒内至少10次变更 → BGSAVE
# save 60 10000 → 60秒内至少10000次变更 → BGSAVE

3.6 实战五:监控持久化健康状态

java 复制代码
@Component
public class PersistenceHealthMonitor {
    @Scheduled(fixedDelay = 30000)
    public void check() {
        String rdbStatus = redis.info("persistence").get("rdb_last_bgsave_status");
        if (!"ok".equals(rdbStatus)) alerts.send("RDB 保存失败!");
        long changes = Long.parseLong(redis.info("persistence").get("rdb_changes_since_last_save"));
        if (changes > 100000) log.warn("RDB 积累变更过多: {} 条", changes);
        long aofDelayed = Long.parseLong(redis.info("persistence").get("aof_delayed_fsync"));
        if (aofDelayed > 0) alerts.send("AOF fsync 延迟! 磁盘可能成为瓶颈");
    }
}

3.7 实战六:恢复演练脚本

bash 复制代码
#!/bin/bash
BACKUP_DIR="/backup/redis"
RESTORE_DIR="/data/restore-test"
cp $BACKUP_DIR/latest/dump.rdb $RESTORE_DIR/
cp $BACKUP_DIR/latest/appendonly.aof $RESTORE_DIR/
docker run --name redis-restore-test -v "$RESTORE_DIR:/data" \
  -d redis:8.6 redis-server --dir /data --appendonly yes
sleep 3
KEYS=$(docker exec redis-restore-test redis-cli DBSIZE)
echo "恢复 key 总数: $KEYS"
docker stop redis-restore-test && docker rm redis-restore-test

3.8 实战七:AOF 文件损坏修复

bash 复制代码
# 检查
redis-check-aof /data/appendonly.aof
# 修复(截断损坏部分)
redis-check-aof --fix /data/appendonly.aof

3.9 实战八:持久化性能压测

bash 复制代码
# everysec: ~80000 QPS (生产默认)
redis-benchmark -t set -n 100000 -c 50 -q
# always: ~2000 QPS (磁盘瓶颈)
redis-benchmark -t set -n 10000 -c 50 -q

3.10 实战九:持久化方案决策树

arduino 复制代码
你的数据丢得起吗?
├── 完全丢得起(纯缓存,可回源)
│   → 可以不开持久化,或只开 RDB 做快速重启
├── 可丢 5 分钟
│   → RDB + save 300 10
├── 可丢 1 秒
│   → AOF everysec + RDB
├── 几乎不能丢
│   → AOF everysec + RDB + 主从复制 + min-replicas-to-write 1
└── 绝对不能丢
    → Redis 不是正确答案,数据库才是最终事实来源

3.11 实战十:磁盘故障应对

bash 复制代码
# 磁盘空间满 → Redis日志 "Can't save in background"
# 解决: 监控磁盘使用率(告警80%)、独立磁盘

# 磁盘IO瓶颈 → aof_delayed_fsync持续增长
# 解决: SSD替代HDD、调整fsync策略、分磁盘

3.12 实战十一:多级数据安全保障

复制代码
Redis 数据安全金字塔:
  数据库副本 ← 最终兜底(异步刷回)
  主从复制   ← 实时副本(异步同步)
  AOF 日志  ← 秒级丢失窗口
  RDB 快照  ← 分钟级备份
  异地备份   ← 灾备(日常定时同步)
每一层独立于上一层,共同构成数据安全体系

4.4 生产事故:AOF 重写导致的 OOM

某支付系统 Redis 内存 20GB,开启了 AOF。某天凌晨自动触发 AOF rewrite------子进程 fork 时 Copy-on-Write 需要额外内存。高峰期间写入量很大,COW 导致内存额外增长 15GB,加上原 redis 的 20GB,总计 35GB------超过机器 32GB 物理内存,触发 OOM Killer,Redis 被系统直接杀掉。教训:① Redis 实例的 maxmemory 加上 fork 期间的 COW 预算不超过物理内存的 70%;② AOF rewrite 尽量安排低峰期执行;③ 监控 used_memory_rss 而不仅是 used_memory。

4.5 持久化配置速查卡

配置 生产建议值 说明
appendonly yes 开启 AOF
appendfsync everysec 99% 场景最优
auto-aof-rewrite-percentage 100 增长 100% 触发重写
auto-aof-rewrite-min-size 64mb 文件小于此值不重写
aof-use-rdb-preamble yes 混合持久化
save 900 1 300 10 60 10000 RDB 触发条件
dbfilename dump.rdb RDB 文件名
aof-load-truncated yes 允许加载截断的 AOF

4.6 持久化深度对话:什么时候 Redis 不适合做主存储

小胖:"我看了文档说 AOF 可以做到 everysec,最大丢 1 秒。那我把订单也存 Redis 行不行?反正最多丢 1 秒嘛。"

大师:"不行。这就是对持久化的误解。AOF everysec 保证的是'Redis 进程正常运行时最多丢 1 秒',但不保证------操作系统 crash 时不丢、磁盘损坏时不丢、Redis bug 导致写坏 AOF 时不丢。这些风险在数据库中有 WAL 日志、doublewrite buffer、checksum 等机制来保障,Redis 没有。Redis 持久化是为了'缓存数据的恢复',不是为了'替代数据库'。"

小白追问:"那什么时候可以将 Redis 作为唯一存储?"

大师:"三个条件同时满足:① 数据可以从其他系统重新生成(如缓存);② 丢失少量数据不会造成资金损失;③ 数据量不会无限增长且 Redis 内存可控。如果任何一个条件不满足------数据库才是正确选择。"

4.7 日常巡检清单

makefile 复制代码
每日检查:
  □ RDB 最近一次 BGSAVE 是否成功? (INFO persistence → rdb_last_bgsave_status)
  □ RDB 距离上次保存的变更数是否合理? (rdb_changes_since_last_save)
  □ AOF 是否有 fsync 延迟? (aof_delayed_fsync > 0 → 告警)
  □ 磁盘剩余空间 > 20%?

每周检查:
  □ 备份文件是否能被 redis-check-rdb/redis-check-aof 验证通过?
  □ 在隔离环境测试恢复流程 (至少抽样验证 DBSIZE)
  □ AOF 文件是否需要手动触发重写?

每月检查:
  □ 持久化配置是否符合业务当前需求?
  □ 全量恢复演练 (从备份恢复完整的 Redis 并验证)

4. 项目总结

4.1 持久化策略选型速查

业务要求 推荐配置 RPO RTO
纯缓存(可回源) 可不开持久化 - -
可丢 5 分钟 RDB (save 300 10) 5 分钟 取决于数据量
可丢 1 秒 AOF everysec 1 秒 取决于 AOF 大小
可丢 1 秒+快速恢复 混合持久化 1 秒 秒级
零丢失 Redis 不适合,须数据库兜底 - -

4.2 fork 的代价

内存大小 fork 耗时(典型) 风险
< 1GB < 1ms 无影响
1-10GB 1-10ms 可接受
10-50GB 10-100ms 需关注延迟告警
> 50GB > 100ms 建议关闭自动 RDB

4.3 思考题

  1. 设计题:如果业务要求最多丢失 1 秒数据,你会选什么持久化方案?如果机房发生断电,你的"最多丢 1 秒"承诺还能兑现吗?

  2. 演练题:设计一个 Redis 数据恢复演练的完整流程------从发现故障、评估影响、选择恢复方案、执行恢复、到验证数据正确性。

(答案提示将在下一章末尾给出。)

4.8 数据恢复完整演练流程

日常巡检只是"体检",真正的数据安全需要通过定期演练来验证。以下是一套经过生产验证的完整恢复演练流程。

演练前提:在隔离环境(Docker 或独立虚拟机)执行,不能影响生产。

bash 复制代码
#!/bin/bash
# recovery_drill.sh ------ Redis 数据恢复完整演练脚本
# 目标:验证 RDB/AOF 备份可恢复、数据完整、耗时可控

set -euo pipefail

DRILL_TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DRILL_DIR="/data/redis-drill/${DRILL_TIMESTAMP}"
BACKUP_DIR="/data/redis-backup"
REDIS_BIN="/usr/local/bin/redis-server"
REDIS_CLI="/usr/local/bin/redis-cli"

echo "=========================================="
echo " Redis 数据恢复演练 - ${DRILL_TIMESTAMP}"
echo "=========================================="

# ============ 阶段 1: 准备演练环境 ============
echo ""
echo "[阶段 1/6] 准备演练环境"
mkdir -p "${DRILL_DIR}"/{data,logs,config}

# 复制最新的 AOF 和 RDB 备份
LATEST_RDB=$(ls -t ${BACKUP_DIR}/dump-*.rdb 2>/dev/null | head -1)
LATEST_AOF=$(ls -t ${BACKUP_DIR}/appendonly-*.aof 2>/dev/null | head -1)

if [ -z "$LATEST_RDB" ] && [ -z "$LATEST_AOF" ]; then
    echo "[失败] 未找到任何备份文件"
    exit 1
fi

echo "  使用 RDB: ${LATEST_RDB:-无}"
echo "  使用 AOF: ${LATEST_AOF:-无}"

# ============ 阶段 2: 备份文件完整性校验 ============
echo ""
echo "[阶段 2/6] 备份文件完整性校验"

if [ -n "$LATEST_RDB" ]; then
    cp "$LATEST_RDB" "${DRILL_DIR}/data/dump.rdb"
    echo "  RDB 文件大小: $(du -h ${DRILL_DIR}/data/dump.rdb | cut -f1)"

    # 用 redis-check-rdb 校验文件完整性
    CHECK_RESULT=$(redis-check-rdb "${DRILL_DIR}/data/dump.rdb" 2>&1)
    if echo "$CHECK_RESULT" | grep -q "Checksum OK"; then
        echo "  RDB 校验: 通过 (Checksum OK)"
    else
        echo "  RDB 校验: 失败!"
        echo "$CHECK_RESULT"
        exit 1
    fi
fi

if [ -n "$LATEST_AOF" ]; then
    cp "$LATEST_AOF" "${DRILL_DIR}/data/appendonly.aof"
    echo "  AOF 文件大小: $(du -h ${DRILL_DIR}/data/appendonly.aof | cut -f1)"

    # 用 redis-check-aof 校验文件
    CHECK_AOF=$(redis-check-aof "${DRILL_DIR}/data/appendonly.aof" 2>&1)
    if echo "$CHECK_AOF" | grep -q "AOF analyzed"; then
        echo "  AOF 校验: 通过"
    else
        echo "  AOF 校验: 存在问题"
        echo "$CHECK_AOF"
        # AOF 截断通常可以修复
        echo "  尝试修复 AOF..."
        redis-check-aof --fix "${DRILL_DIR}/data/appendonly.aof"
    fi
fi

# ============ 阶段 3: 启动演练 Redis 实例 ============
echo ""
echo "[阶段 3/6] 启动演练 Redis 实例"

cat > "${DRILL_DIR}/config/redis.conf" <<EOF
port 16379
dir ${DRILL_DIR}/data
logfile ${DRILL_DIR}/logs/redis.log
dbfilename dump.rdb
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes
requirepass drill-password
maxmemory 2gb
EOF

# 启动 Redis
$REDIS_BIN "${DRILL_DIR}/config/redis.conf" --daemonize yes
sleep 3

# 等待 Redis 加载完成
for i in $(seq 1 60); do
    if $REDIS_CLI -p 16379 -a drill-password --no-auth-warning PING 2>/dev/null | grep -q PONG; then
        echo "  Redis 启动成功"
        break
    fi
    if [ $i -eq 60 ]; then
        echo "  Redis 启动超时!"
        tail -20 "${DRILL_DIR}/logs/redis.log"
        exit 1
    fi
    sleep 1
done

# 检查加载过程中是否有 AOF 错误
if grep -q "Short read\|Bad file format" "${DRILL_DIR}/logs/redis.log"; then
    echo "  [警告] AOF 加载有错误,部分数据可能丢失"
    grep "Short read\|Bad file format\|truncated" "${DRILL_DIR}/logs/redis.log"
fi

# ============ 阶段 4: 数据完整性验证 ============
echo ""
echo "[阶段 4/6] 数据完整性验证"

# 获取数据总量
DBSIZE=$($REDIS_CLI -p 16379 -a drill-password --no-auth-warning DBSIZE 2>/dev/null)
echo "  恢复 key 总数: ${DBSIZE}"

# 抽样验证关键业务 key
SAMPLE_KEYS=(
    "product:cache:100"
    "user:session:admin"
    "inventory:seckill:2026"
    "rank:daily:score"
)

echo "  抽样验证:"
for KEY in "${SAMPLE_KEYS[@]}"; do
    if [ -n "$($REDIS_CLI -p 16379 -a drill-password --no-auth-warning EXISTS "$KEY" 2>/dev/null)" ]; then
        EXISTS=$($REDIS_CLI -p 16379 -a drill-password --no-auth-warning EXISTS "$KEY" 2>/dev/null)
        if [ "$EXISTS" = "1" ]; then
            TYPE=$($REDIS_CLI -p 16379 -a drill-password --no-auth-warning TYPE "$KEY" 2>/dev/null)
            echo "    ✓ $KEY (type=$TYPE)"
        else
            echo "    ○ $KEY (不存在,可能已过期)"
        fi
    fi
done

# 随机抽样 100 个 key,检查有无损坏
if [ "$DBSIZE" -gt 0 ]; then
    SAMPLE=$($REDIS_CLI -p 16379 -a drill-password --no-auth-warning RANDOMKEY 2>/dev/null)
    echo "  随机 key 测试: $(echo $SAMPLE | head -5)"
fi

# 检查内存使用
MEM=$($REDIS_CLI -p 16379 -a drill-password --no-auth-warning INFO memory 2>/dev/null | grep used_memory_human | cut -d: -f2 | tr -d '\r')
echo "  恢复后内存: ${MEM}"

# ============ 阶段 5: 性能测试 ============
echo ""
echo "[阶段 5/6] 性能基准测试"

# 写操作延迟
START=$(date +%s%3N)
$REDIS_CLI -p 16379 -a drill-password --no-auth-warning SET drill:perf:test "benchmark" > /dev/null 2>&1
END=$(date +%s%3N)
WRITE_LATENCY=$((END - START))
echo "  SET 延迟: ${WRITE_LATENCY}ms"

# 读操作延迟
START=$(date +%s%3N)
$REDIS_CLI -p 16379 -a drill-password --no-auth-warning GET drill:perf:test > /dev/null 2>&1
END=$(date +%s%3N)
READ_LATENCY=$((END - START))
echo "  GET 延迟: ${READ_LATENCY}ms"

# 简单压测
BENCH_RESULT=$($REDIS_CLI -p 16379 -a drill-password --no-auth-warning --csv \
    --latency-history 2>&1 | head -5)
echo "  延迟分布 (前 5 条): $BENCH_RESULT"

# ============ 阶段 6: 清理与报告 ============
echo ""
echo "[阶段 6/6] 清理与生成报告"

# 优雅关闭演练 Redis
$REDIS_CLI -p 16379 -a drill-password --no-auth-warning SHUTDOWN NOSAVE 2>/dev/null || true

REPORT_FILE="${DRILL_DIR}/recovery_report.md"

cat > "$REPORT_FILE" <<REPORT
# Redis 数据恢复演练报告

**演练时间**: ${DRILL_TIMESTAMP}
**演练人员**: $(whoami)
**服务器**: $(hostname)

## 备份信息
- RDB: ${LATEST_RDB:-无} ($(du -h ${LATEST_RDB:-/dev/null} 2>/dev/null | cut -f1))
- AOF: ${LATEST_AOF:-无} ($(du -h ${LATEST_AOF:-/dev/null} 2>/dev/null | cut -f1))

## 恢复结果
- 恢复 key 总数: ${DBSIZE}
- 恢复后内存: ${MEM}
- SET 延迟: ${WRITE_LATENCY}ms
- GET 延迟: ${READ_LATENCY}ms

## 校验状态
- RDB 校验: $( [ -n "$LATEST_RDB" ] && echo "通过" || echo "未使用" )
- AOF 校验: $( [ -n "$LATEST_AOF" ] && echo "通过" || echo "未使用" )

## 结论
- 备份可恢复: ✓
- 数据完整性: $( [ "$DBSIZE" -gt 0 ] && echo "✓ (${DBSIZE} keys)" || echo "✗ (0 keys)" )
- 恢复耗时: $(grep "加载" ${DRILL_DIR}/logs/redis.log | tail -1 || echo "见日志")
- 建议: $( [ "$DBSIZE" -gt 0 ] && echo "备份方案可用" || echo "需检查备份策略" )

## 日志位置
- Redis 日志: ${DRILL_DIR}/logs/redis.log
- 恢复数据: ${DRILL_DIR}/data/
REPORT

echo "  报告已生成: $REPORT_FILE"
echo ""
echo "============ 演练完成 ============"
cat "$REPORT_FILE"

4.9 常见生产持久化事故与应对手册

持久化相关的生产事故往往在"最需要 Redis 的时候"发生------流量高峰、内存紧张、磁盘告急。以下是几种高频事故及标准应对流程。

事故一:磁盘写满导致 AOF 写入失败

bash 复制代码
事故现场:
  - 应用日志出现: MISCONF Redis is configured to save RDB snapshots
  - Redis 拒绝所有写入(因为 stop-writes-on-bgsave-error 默认 yes)
  - 或者: AOF 写入失败,Redis 进程持续但无持久化

紧急处理(按优先级):
  ① 清理磁盘空间(删旧日志/临时文件/旧的 RDB 备份)
     df -h /data/redis
     find /data/redis-backup -name "dump-*.rdb" -mtime +7 -delete

  ② 如果短时间无法释放空间,临时重定向持久化到其他磁盘
     CONFIG SET dir /data2/redis-data  (需确保目录有写权限)
     CONFIG REWRITE  (持久化到配置文件)

  ③ 监控恢复
     watch -n 5 'redis-cli INFO persistence | grep -E "aof_last_write_status|rdb_last_bgsave_status"'
     确认状态变为 "ok"

根本解决:
  - 磁盘使用率 > 80% 自动告警
  - 定期清理过期备份(保留最近 7 天)
  - AOF 文件单独磁盘(独立 IO)
  - 监控 dir 所在磁盘的可用空间

事故二:BGSAVE 期间 fork 导致延迟飙升

markdown 复制代码
事故现场:
  - 突然出现大量慢查询(GET/SET > 100ms)
  - Redis 日志: "Background saving started by signal"
  - 系统 sar 显示: 内存使用快速增长
  - used_memory_rss >> used_memory

原因分析:
  BGSAVE fork 触发 Copy-on-Write。如果 fork 期间主进程有大量写入,
  操作系统需要复制被修改的内存页,导致:
  - 内存额外增长(COW 开销)
  - 父进程等待内存页复制 → 命令延迟增加

紧急处理:
  ① 如果未 OOM → 等待 BGSAVE 完成(不要 kill,可能损坏 RDB)
  ② 如果逼近 OOM → 适当降低 maxmemory 腾出 COW 空间
     CONFIG SET maxmemory <当前值 - 20%>

  ③ 临时停止自动 RDB
     CONFIG SET save ""  (关闭自动触发)
     等低峰期手动触发: BGSAVE

预防措施:
  - maxmemory 设置不超过物理内存的 50%(预留 COW 空间)
  - 监控 used_memory_rss / used_memory 比例 > 1.5 → 告警
  - 大内存实例(> 32GB)关闭自动 RDB,改为定时脚本手动 BGSAVE
  - 开启 THP (Transparent Huge Pages) 禁用:
    echo never > /sys/kernel/mm/transparent_hugepage/enabled

事故三:AOF 文件损坏无法启动

bash 复制代码
事故现场:
  - Redis 启动时 panic: "Bad file format reading the append only file"
  - 或: "Short read or OOM loading DB. Unrecoverable error"

紧急处理:
  ① 备份受损的 AOF 文件(不在当前目录)
     cp appendonly.aof appendonly.aof.damaged.$(date +%Y%m%d%H%M%S)

  ② 用 redis-check-aof 修复
     redis-check-aof --fix appendonly.aof
     # 输出示例:
     # AOF analyzed: size=128849018, ok_up_to=128840000, diff=9018
     # 截断到 ok_up_to 位置 → 损失最后 9KB 数据

  ③ 手动删除最后损坏的命令(如果 --fix 失败)
     # 找到最后一个完整命令的位置
     grep -n "^\*" appendonly.aof | tail -5
     # 假设最后一个完整的 SET 在第 50000 行
     # 截断到该行之前
     head -n 50000 appendonly.aof > appendonly.aof.fixed

  ④ 如果 AOF 完全损坏,回退到 RDB
     CONFIG SET appendonly no  (临时关闭 AOF)
     使用最新的 RDB 文件恢复
     从 RDB 启动后,再重新开启 AOF 并触发重写
     CONFIG SET appendonly yes
     BGREWRITEAOF

  ⑤ 数据补偿
     - 从业务数据库回填缓存数据
     - 从消息队列(Stream/Kafka)重放丢失的写入
     - 通知业务方数据可能不完整

4.10 多层级备份策略设计

单点备份的可靠性永远不足------"你的备份文件也可能坏"。需要多层级、多地域、多时间维度的备份策略。

yaml 复制代码
备份层级金字塔:

┌─────────────────────────────────┐
│  L4: 异地灾备(每日一次)          │  ← 另一个机房/云 Region
│  存储: 对象存储 (S3/OSS/COS)      │    保留 30 天
├─────────────────────────────────┤
│  L3: 本地完整备份(每 4 小时)     │  ← 同机房不同机器
│  存储: NAS / 分布式文件系统        │    保留 7 天
├─────────────────────────────────┤
│  L2: AOF 持续写入                │  ← 本机磁盘
│  存储: 独立 SSD 磁盘              │    实时
├─────────────────────────────────┤
│  L1: 主从复制(异步)             │  ← 同机房/跨机房
│  实时副本                        │    秒级延迟
└─────────────────────────────────┘

备份脚本示例(带完整性校验和上传):

bash 复制代码
#!/bin/bash
# redis_multilevel_backup.sh ------ 多层级备份脚本

BACKUP_ROOT="/data/redis-backup"
REMOTE_STORAGE="s3://my-redis-backup/daily/"
REDIS_HOST="127.0.0.1"
REDIS_PORT=6379
REDIS_PASS="your-password"
BACKUP_TIMESTAMP=$(date +%Y%m%d_%H%M%S)

# ===== L2: 本机 AOF 临时快照 =====
echo "[L2] 创建本地备份..."
BACKUP_DIR="${BACKUP_ROOT}/${BACKUP_TIMESTAMP}"
mkdir -p "$BACKUP_DIR"

# 触发 BGSAVE,等待完成
redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASS --no-auth-warning BGSAVE
while true; do
    STATUS=$(redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASS --no-auth-warning \
        INFO persistence | grep rdb_bgsave_in_progress | cut -d: -f2 | tr -d '\r')
    if [ "$STATUS" = "0" ]; then break; fi
    echo "  等待 BGSAVE 完成..."
    sleep 5
done

# 复制 RDB(带 checksum)
RDB_FILE=$(redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASS --no-auth-warning \
    CONFIG GET dir | tail -1)/dump.rdb
cp "$RDB_FILE" "${BACKUP_DIR}/dump.rdb"
sha256sum "${BACKUP_DIR}/dump.rdb" > "${BACKUP_DIR}/dump.rdb.sha256"

# 同时备份 AOF(如果开启)
AOF_DIR=$(redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASS --no-auth-warning \
    CONFIG GET dir | tail -1)
if [ -f "${AOF_DIR}/appendonly.aof" ]; then
    cp "${AOF_DIR}/appendonly.aof" "${BACKUP_DIR}/appendonly.aof"
    sha256sum "${BACKUP_DIR}/appendonly.aof" > "${BACKUP_DIR}/appendonly.aof.sha256"
fi

echo "  本地备份完成: ${BACKUP_DIR}"

# ===== L3: 同步到 NAS =====
echo "[L3] 同步到 NAS..."
NAS_MOUNT="/mnt/redis-nas"
if mountpoint -q "$NAS_MOUNT"; then
    NAS_BACKUP="${NAS_MOUNT}/$(date +%Y%m%d)"
    mkdir -p "$NAS_BACKUP"
    rsync -avz --checksum "${BACKUP_DIR}/" "${NAS_BACKUP}/"
    echo "  NAS 同步完成"
else
    echo "  [警告] NAS 未挂载,跳过 L3 备份"
fi

# ===== L4: 同步到 S3 异地存储 =====
echo "[L4] 同步到 S3..."
if command -v aws &> /dev/null; then
    aws s3 cp "${BACKUP_DIR}/dump.rdb" \
        "${REMOTE_STORAGE}dump-${BACKUP_TIMESTAMP}.rdb" \
        --storage-class STANDARD_IA &> /dev/null \
        && echo "  S3 上传完成" \
        || echo "  [警告] S3 上传失败"

    # 上传校验文件
    aws s3 cp "${BACKUP_DIR}/dump.rdb.sha256" \
        "${REMOTE_STORAGE}dump-${BACKUP_TIMESTAMP}.rdb.sha256" &> /dev/null
else
    echo "  [警告] aws CLI 未安装,跳过 S3 备份"
fi

# ===== 清理旧备份 =====
echo "[清理] 删除 7 天前的本地备份..."
find "${BACKUP_ROOT}" -type d -mtime +7 -exec rm -rf {} \; 2>/dev/null

echo "[完成] 备份时间戳: ${BACKUP_TIMESTAMP}"

4.11 持久化选型对话------四个常见误区

小胖:"我有个闺蜜公司的后端,说 Redis 持久化就是'开了就行',从来不管。这样行吗?"

大师:"四个常见误区,我们一个个过。"

误区一:'开了 AOF 就万无一失'

你以为 实际
AOF everysec = 最多丢 1 秒 机器断电时,fsync 可能没执行,可能丢最后几秒
AOF 文件是完整的 文件末尾可能因磁盘故障被截断
AOF 重写是安全的 重写时磁盘 IO 打满,影响业务请求延迟

误区二:'RDB 恢复了就好,不用验证'

makefile 复制代码
某创业公司的真实案例:
  RDB 文件拷到新机器 → 启动 → DBSIZE 和预期一致 → 宣布恢复成功
  但 2 小时后发现:
  - 10% 的 Hash key 内部 field 丢失(RDB 写入时 COW 页面未完全同步)
  - 5% 的 ZSET key score 值异常(ZSET 编码转换过程中 BGSAVE)

教训: 恢复后必须抽样验证数据正确性,不能只看 DBSIZE。

误区三:'混合持久化是银弹'

markdown 复制代码
混合持久化 (aof-use-rdb-preamble yes):
  AOF 文件 = RDB 快照(头部)+ AOF 增量(尾部)

优点: 恢复速度快(前半段 RDB + 后半段增量回放)
缺点: 
  - 文件可读性差无法手动编辑
  - 如果 RDB 部分损坏,整个文件不可用(不像纯 AOF 可以 truncate)
  - 子进程 BGREWRITEAOF 期间仍有 COW 开销

小白:"那生产环境到底怎么选?"

大师的决策矩阵:

objectivec 复制代码
  数据可丢失 5+ 分钟? → YES → RDB only
  └─ NO → 数据可丢失 1-2 秒? → YES → AOF everysec
      └─ NO → 需要快速恢复 + 低丢失? → YES → 混合持久化
          └─ NO → 零丢失? → Redis 不适合,数据库 + 事件溯源

上一章思考题答案提示(第21章):

第1题(配置中心完整流程):变更------运营修改→写 MySQL→写 Redis key→PUBLISH config:changed {key, version}。读取------启动时从 Redis/MySQL 全量加载,运行时订阅 config:changed,收到通知后按 key 拉取最新值。兜底------每 5 分钟对比本地版本号和 Redis key 中的版本。

第2题(40% 服务未生效):① PUBLISH 瞬间离线;② 订阅连接创建失败无告警;③ 本地版本号大于收到版本号,忽略了消息。

相关推荐
没有气的汽水20 分钟前
升级完客户端,redis 又开始大量返回 MOVED:一个埋了七个月的本地配置
redis
csdn_aspnet29 分钟前
如何在本地 Docker 容器中运行 Redis,并使用 Redis Insight 和 Redis CLI 进行管理
redis·docker·容器
Decay__43 分钟前
22_MySQL 运维自动化平台 —— 部署编排 + 监控 + 巡检 + 告警 + 工单闭环一体化
运维·数据库·sql·mysql·架构
Cosolar2 小时前
极简智库(MiniDocs):docsify、VitePress、Notion 等知识库搭建服务的完美替代
前端·后端·架构
Decay__2 小时前
21_MySQL 优化案例集 —— 慢 SQL 优化、索引重构与参数调整收益对比
运维·数据库·sql·mysql·架构
vipxieliang3 小时前
事件驱动架构实战:解耦、异步与可扩展性
架构
mldong5 小时前
同一个节点接力办几轮,流程才往下走:jeeflow 的后加签
后端·架构
IT规划师5 小时前
Redis 数据类型全解 - Bitfield:99% 开发者没用过的位域操作,把内存再砍 80%(七)
redis·bitfield·位域