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 秒"承诺还能兑现吗?
-
演练题:设计一个 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 瞬间离线;② 订阅连接创建失败无告警;③ 本地版本号大于收到版本号,忽略了消息。