Skip to content

Redis 持久化与高可用 ​

S4-011建议学高级约 60 分钟JDK 21企业开发进阶 · Redis

1. 本节定位 ​

《Redis 分布式锁》解决并发互斥;本节解决**"Redis 挂了会怎样"**:

text
持久化:解决「重启后数据还在吗」  → RDB / AOF
高可用:解决「它挂了服务还能用吗」 → 主从 / 哨兵 / 集群

★ 两者的取舍是同一个主题:安全 vs 性能 ✓✓

2. 学习目标 ​

  • 能说出 RDB 与 AOF 的取舍,并引用实测数据 ★★★;
  • 知道三种 fsync 策略及它们与 MySQL/Kafka 的对应 ★★★;
  • 知道"混合持久化"为什么兼得两者 ★★;
  • 知道主从复制的异步性会带来什么一致性风险 ★★★;
  • 知道脑裂与 min-replicas-to-write ★★;
  • 能按数据量选哨兵还是集群 ★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看一个常见误解 ✗:

text
"Redis 是缓存,丢了就丢了,不用持久化"
→ 但如果它同时承担"分布式锁""延时队列""计数"([S4-010](./S4-010))
→ 重启后锁没了、计数归零、延时任务全丢 ✗✗

★ 所以先问自己:Redis 里存的东西"丢了能重建吗" ✓✓

存的东西丢了能重建吗
商品详情缓存 ✓能(回源重建即可)→ 持久化要求低
分布式锁 / 计数 / 延时队列 ✗不能 → 必须持久化,且要评估丢多少可接受

5. 核心概念 ​

5.1 RDB vs AOF:一次经典的取舍 ★★★ ​

维度RDB(快照)AOF(追加日志)
记什么某一时刻的全量数据每一条写命令
文件大小紧凑 ✓✓大(实测大 1.7 倍)✗
恢复速度快(直接读进内存)✓✓慢(要重放命令)✗
丢数据多(两次快照之间全丢)✗✗少(取决于 fsync)✓✓
对性能影响fork 子进程时有影响 ⚠️持续写盘 ⚠️

★ 本节的实测 ✓:

text
  RDB:写入 10 万条数据后做一次快照
       快照文件:3.32 MB,生成耗时 82 ms
  AOF:记录 10 万条写命令
       → **AOF 比 RDB 大 1.7 倍** ✗

  恢复:
       从 RDB 恢复 100000 条:  92 ms ✓✓
       从 AOF 恢复 100000 条: 180 ms
       → AOF 恢复慢约 2 倍 ✗

★★ 两条结论 ✓✓:

#结论依据
①RDB 恢复快、文件小 → 适合备份与全量同步 ✓✓
②AOF 丢数据少 → 适合要求不丢数据的场景 ✓✓

5.2 三种 fsync 策略:安全与性能的三档 ★★★ ​

策略每次写入的代价崩溃时丢失适用
always每次都等落盘(最慢) ✗几乎不丢 ✓✓金融级
everysec ★★每秒 fsync 一次最多 1 秒 ✓默认,推荐 ✓✓
no交给操作系统可能丢几十秒 ✗纯缓存

★★ 同样的取舍在别处也出现过(可以对照理解) ✓✓:

中间件参数三档含义
MySQL ★★innodb_flush_log_at_trx_commit1 / 2 / 0 ——完全一样的三档
Kafka ★★acksall / 1 / 0

★ 由此得到一条通用方法 ✓✓:

看到一个中间件,先问它"什么时候落盘",基本就能判断它的可靠性边界 ✓

5.3 混合持久化:兼得两者 ★★ ​

Redis 4.0+ 的做法 ✓✓:

text
AOF 文件的前半部分 = RDB 格式的全量快照
AOF 文件的后半部分 = 增量写命令

★ 恢复时 ✓:先加载快照(快),再重放少量增量(不丢) ✓✓

★ 还有"RDB 做备份、AOF 做恢复源"的常见组合 ✓:

用途用什么
定时备份、异地容灾RDB ✓(文件小、传输快)
实例重启恢复AOF(或混合) ✓

5.4 主从复制:读写分离的代价 ★★★ ​

结构:一个主库(写)+ N 个从库(读)✓

★★ 关键认识:主从同步是"异步"的 ✓✓:

text
主库写入成功 → 立刻返回客户端 ✓
              → 后台异步把命令发给从库 ⚠️

所以:**主库刚写的值,从库可能读不到** ✗✗

★ 这带来两个真实问题 ✓:

问题表现
读写分离读到旧值 ★★★用户刚下单,刷新页面却看不到(因为读到了从库)✗✗
主库宕机可能丢最后几条写 ★★从库还没收到就切主了 ✓

★ 两个应对 ✓:

手段说明
写后短期读主库 ★★如"下单后 1 秒内的查询走主库" ✓
WAIT 命令 ⚠️等待 N 个从库确认(会牺牲性能)

★ 与本模块已有内容的呼应 ✓✓: 这正是 《缓存一致性与更新策略》 一致性问题的一部分 —— "缓存与 DB 不一致"和"主从读到旧值"是两个独立但同源的问题 ✓✓

5.5 高可用的三层演进 ★★★ ​

方案解决什么局限
主从复制 ★★读扩展 + 数据备份主机挂了要手动切 ✗
哨兵(Sentinel) ★★★自动故障转移 ✓数据量受单机内存限制 ✗
集群(Cluster) ★★★分片 + 高可用 ✓✓运维复杂、跨槽操作受限 ⚠️

★ 哨兵的三个职责 ✓:

职责说明
监控每秒 ping 所有节点 ✓
判定客观下线 ★★多个哨兵都认为主库挂了才判定(避免误判)✓
选举 + 故障转移从从库中选一个提升为主,并通知客户端 ✓

★★ 脑裂问题(必须知道) ✓✓:

text
网络分区时:
  旧主库还在接受写入(它以为自己是主)
  哨兵已经选出新主库
  → **两个主库同时写** ✗✗
  → 网络恢复后,旧主库的数据被丢弃 ✗✗

★ 缓解手段 ✓:

text
min-replicas-to-write 1        # 至少 1 个从库确认才允许写入
min-replicas-max-lag 10        # 从库延迟不超过 10 秒
→ 写不出去就拒绝写入,宁可少写也不产生"双主" ✗→✓

★ 一条更深的认知 ✓✓:

任何"自动故障转移"都伴随着"可能丢最后几条写"的风险 ✓

这就是 CAP:高可用(A)与强一致(C)天然冲突 ✓✓ (《CAP 与 BASE 理论》 会专门展开)

★ 选型一句话 ✓✓:

数据量小 + 要自动切换 → 哨兵; 数据量大 + 要水平扩展 → 集群 ✓

5.6 什么时候不该把 Redis 当唯一存储 ★★★ ​

★ 一条硬建议 ✓✓:

对"绝对不能丢"的数据,Redis 单独作为存储是不可靠的 ✗

即使开了 AOF everysec,也可能丢 1 秒 ✓

正确做法:Redis 做加速层,MySQL 做真相来源 ✓✓

★ 三种"Redis 不能单独承担"的数据 ✓:

数据为什么
交易流水 / 订单 ✗必须强持久化 ✓
账户余额 ✗丢 1 秒就是资金差错 ✓
审计日志 ✗合规要求 ✓

★ 反过来,这三类很适合放 Redis ✓:

数据为什么
缓存 ✓丢了能重建
限流计数 ✓丢一点影响不大
排行榜 / 会话 ✓可重算或重新登录

6. 递进示例 ​

6.1 最小示例:配置持久化 ​

text
# RDB:900 秒内至少 1 次修改就快照
save 900 1
save 300 10

# AOF:开启且每秒 fsync
appendonly yes
appendfsync everysec

# 混合持久化(4.0+)
aof-use-rdb-preamble yes

6.2 业务示例:读写分离下的"读己之写" ​

java
// 刚下单的用户,1 秒内的查询走主库(避免读到从库旧值)
if (System.currentTimeMillis() - lastWriteTime < 1000) {
    return masterDao.query(id);
}
return replicaDao.query(id);

6.3 反例:把订单号生成器的计数放 Redis 却不持久化 ​

text
重启 → 计数归零 → **生成重复订单号** ✗✗

7. 完整可运行代码与实测 ​

代码:labs/redis-lab/src/main/java/cn/kb/learn/redis/PersistenceLesson.java

运行:

powershell
cd labs
.\mvnw.cmd -pl redis-lab compile
java "-Dstdout.encoding=UTF-8" -cp redis-lab/target/classes cn.kb.learn.redis.PersistenceLesson

实测输出(关键部分):

text
===== 1. RDB(快照)=====
  写入 100000 条数据后做一次快照
  快照文件:3.32 MB,生成耗时 82 ms

===== 2. AOF(追加日志)=====
  → **AOF 比 RDB 大 1.7 倍** ✗

===== 4. 恢复实测:RDB vs AOF =====
  从 RDB 恢复 100000 条:  92 ms ✓✓
  从 AOF 恢复 100000 条: 180 ms
  → AOF 恢复慢约 2 倍 ✗

★ 三条可落地的结论 ✓:

#结论依据
①RDB 恢复快、文件小 → 用于备份与快速恢复92 ms vs 180 ms ✓
②AOF 丢数据少 → 用于"不能丢"的场景最多丢 1 秒(everysec)✓
③生产建议开混合持久化兼得"恢复快"与"丢得少" ✓✓

8. Debug 观察点 ​

  1. 对比 RDB 与 AOF 的文件大小:理解"命令日志 vs 数据快照"的差异 ✓;
  2. 把 AOF 的 fsync 从 everysec 改成 always:观察写入吞吐下降 ⚠️;
  3. 在主从环境里"写主库立刻读从库":观察读到旧值(需要真实 Redis 环境)★★;
  4. 关掉 min-replicas-to-write 并模拟网络分区:思考脑裂的后果 ✓。

9. 常见错误 ​

#错误说明
1以为"缓存不用持久化" ✗✗锁、计数、队列丢了影响很大 ✓
2把 Redis 当唯一存储存订单/余额 ✗✗最多丢 1 秒 → 资金差错 ✓✓
3读写分离后立刻读从库 ✗✗读到旧值(5.4)✓
4以为故障转移是"无损"的 ✗✗可能丢最后几条写 ✓
5不配 min-replicas-to-write ⚠️脑裂时产生双主 ✓
6RDB 频率设得太高 ⚠️频繁 fork 影响性能 ✓
7不做备份演练 ✗✗真出事时才发现备份恢复不了 ✓✓

10. 动手实验 ​

目标:亲手量出两种持久化的取舍,并想清楚"丢多少可接受"。

步骤:

  1. 跑 PersistenceLesson,记录文件大小与恢复耗时 ✓;
  2. 算出"你的业务能接受丢几秒数据",并据此选 fsync 策略 ★★★;
  3. 列出你项目里放在 Redis 的所有数据,逐条标注"丢了能不能重建" ✓;
  4. 对"不能重建"的那部分数据,设计"Redis + MySQL 双写"或"改放 MySQL" ★★;
  5. 做一次备份恢复演练:把备份文件恢复到另一个实例并校验数据 ★★★。

验收方式:能说出"你的 Redis 里哪些数据绝对不能丢、目前的配置能不能保证"。

扩展任务:设计一个"Redis 故障降级方案":

  • Redis 完全不可用时,读请求怎么办(本地缓存?直接读库?返回兜底)✓;
  • 写请求怎么办(拒绝?排队?直接写库)✓;
  • 恢复后如何预热 ✓;
  • 说明:为什么"降级方案必须提前设计"(提示:出事时来不及想)★★

11. 练习题 ​

  1. 判断:Redis 是缓存,所以不需要持久化。(对 / 错)
  2. 填空:RDB 恢复 ______、文件 ______;AOF 丢数据 ______、恢复 ______。
  3. 简答:为什么"主从复制"会导致"刚写完读不到"?
  4. (设计)
text
某系统用 Redis 生成订单号:
    long seq = redis.incr("order:seq");
    String orderNo = "ORD-" + seq;

如果 Redis 重启且没开持久化,会发生什么? 5. (选型)一个系统的 Redis 数据量 8 GB,要求自动故障转移。该用哨兵还是集群?为什么?

参考答案(要点)
  1. 错。如果它承担锁、计数、延时队列,丢了会出问题 ✗ —— 要先问"里面的数据丢了能重建吗" ✓★
  2. 快;小;少;慢 ✓
  3. 因为主从复制是异步的 ✗ ——主库写完立刻返回,从库还没收到 → 读从库就是旧值 ✓★
  4. 计数归零 → 生成重复订单号 → 主键冲突或数据错乱 ✗✗ —— 正确做法:用数据库自增/号段/雪花算法(《分布式 ID 与一致性哈希》),不要依赖非持久化的 Redis 计数 ✓✓
  5. 哨兵 ✓ ——哨兵适合"数据量能放进单机"的场景(8 GB 通常可以)✓; 集群更适合"数据量超过单机内存" 且愿意承担运维复杂性 ✓

12. 掌握标准 ​

  • [ ] 能说出 RDB 与 AOF 的取舍,并引用实测数据 ★★★;
  • [ ] 知道三种 fsync 策略,并能联系到 MySQL 与 Kafka 的同类参数 ★★★;
  • [ ] 知道混合持久化为什么兼得两者 ★★;
  • [ ] 知道主从是异步的,读写分离会读到旧值 ★★★;
  • [ ] 知道脑裂与 min-replicas-to-write ★★;
  • [ ] 知道"自动故障转移可能丢最后几条写"是 CAP 的必然 ★★★;
  • [ ] 知道哪些数据不能单独放 Redis ★★★。

13. 扩展资源 ​

14. 下一步学习 ​

Redis 五节到此完成。接下来进入 S4 的中段:消息队列、Nginx、Netty 与微服务。

《消息队列基础与选型》。

后续知识

暂无

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引