From dda0d6927c2790924a88bbafc0e8d12b3c641edd Mon Sep 17 00:00:00 2001
From: hhs <386998068@qq.com>
Date: Fri, 22 May 2026 00:25:59 +0800
Subject: [PATCH] vault backup: 2026-05-22 00:25:59
---
hhs/Redis/04-RDB持久化.md | 518 ++++++++++++++++++++++++++-----------
hhs/Redis/05-AOF持久化.md | 249 ++++++++++++------
hhs/Redis/06-主从与哨兵.md | 493 ++++++++++++++++++++++++-----------
3 files changed, 888 insertions(+), 372 deletions(-)
diff --git a/hhs/Redis/04-RDB持久化.md b/hhs/Redis/04-RDB持久化.md
index f9edd7b..ff21b46 100644
--- a/hhs/Redis/04-RDB持久化.md
+++ b/hhs/Redis/04-RDB持久化.md
@@ -7,51 +7,59 @@ create time: 2026-05-15 18:12
## 概述
-> [!SUMMARY] RDB 是什么?
-> RDB(Redis Database)是 Redis 最早的持久化机制,通过在特定时间点将内存中的数据集写入磁盘形成**快照文件(.rdb)**,实现数据持久化。
+> [!QUESTION] Redis 的数据都存在内存里,那服务器一断电,数据岂不是全没了?
+> 没错——这就是**持久化**存在的意义。Redis 提供两种持久化方案:**RDB** 和 **AOF**,本文先讲 RDB。
+
+> [!SUMMARY] 一句话理解 RDB
+> RDB(Redis Database)就是在**某个时间点**,把 Redis 内存中的**全部数据**"拍一张照片"存到磁盘上——这个"照片"就是一个 `.rdb` **快照文件**。
+>
+> 你可以把它想象成**游戏存档**:打 Boss 之前你手动存了一次档,万一翻车了可以读档重来。但代价是——**存档之后到翻车之间**的操作全丢了。
### 核心特点
-| 特性 | 说明 |
-|------|------|
-| ⚡ 恢复极快 | 二进制文件直接加载,避免命令回放 |
-| 📦 文件紧凑 | 压缩存储,远小于等量 AOF 日志 |
-| 🔧 架构简单 | 无额外复杂逻辑,适合备份和迁移 |
-| ⚠️ 数据窗口丢失 | 两次快照之间的写操作不可恢复 |
+| 特性 | 说明 | 类比 |
+|------|------|------|
+| ⚡ 恢复极快 | 二进制文件直接加载到内存,跳过命令回放 | 像从压缩包解压,比照着菜谱重新做菜快得多 |
+| 📦 文件紧凑 | 使用 LZF 压缩,远小于等量 AOF 日志 | 一张照片的信息量 > 千言万语的描述 |
+| 🔧 架构简单 | 无额外复杂逻辑,一个文件搞定备份/迁移 | "复制-粘贴"级别的简单 |
+| ⚠️ 数据窗口丢失 | 两次快照之间的写操作不可恢复 | 存档点之后的操作无法回溯 |
### 何时使用
-- **大数据集冷备**:定期全量备份到 S3 / OSS
-- **灾难恢复**:配合 CI/CD 快速重建实例
-- **大规模数据场景**:恢复速度优先于毫秒级数据一致性
+- **大数据集冷备**:定期全量备份到 S3 / OSS,作为最后一道防线
+- **灾难恢复**:服务器挂了?把 `.rdb` 文件丢到新机器上,秒级恢复
+- **大规模数据场景**:当你的 Redis 存了 50GB 数据,AOF 回放可能要几十分钟,RDB 几秒搞定
+- **主从复制**:新从节点首次全量同步时,主节点会自动触发 RDB 快照传给从节点
> [!QUESTION] 为什么 RDB 恢复比 AOF 快?
-> RDB 是直接加载二进制快照到内存,AOF 则需要逐条回放命令——数据量越大差距越明显。
+> 打个比方:RDB 就像**拍了一张照片**,加载时直接"看照片"还原场景;AOF 则像**记了一本流水账**("先加了张三,再删了李四,又改了王五……"),恢复时需要从头到尾把账本念一遍——数据量越大,"念账本"的时间就越长。
## 触发机制
### 手动触发
```bash
-# 当前线程执行,阻塞主线程 ❌
+# 当前线程执行,阻塞主线程 ❌ 生产环境禁用!
SAVE
-# 主进程 fork 子进程在后台完成(推荐)
+# 主进程 fork 子进程在后台完成(✅ 推荐)
BGSAVE
-# 查看上次 BGSAVE 状态
+# 查看上次 BGSAVE 成功的时间戳
LASTSAVE
```
-| 命令 | 行为 | 影响 |
-|------|------|------|
-| `SAVE` | 同步保存,主线程阻塞 | **生产环境禁用!** |
-| `BGSAVE` | fork 子进程异步保存 | 主线程继续服务,但触发 fork 瞬间短暂停顿 |
-| `BGREWRITEAOF` | AOF rewrite | 与 BGSAVE 可同时运行(不同子进程) |
+| 命令 | 行为 | 适用场景 | 风险 |
+|------|------|----------|------|
+| `SAVE` | 同步阻塞,直到快照完成 | 紧急调试,或确认实例无流量时 | **主线程完全卡死**——所有客户端请求排队等待 |
+| `BGSAVE` | fork 子进程异步完成 | 生产环境日常备份 | fork 瞬间有极短暂的阻塞(通常毫秒级) |
+
+> [!QUESTION] 为什么 SAVE 危险但 Redis 还要提供它?
+> 因为 `SAVE` 可以在 **BGSAVE 正在执行时** 强制同步保存(BGSAVE 期间再次 BGSAVE 会被拒绝)。这是一个极端场景的"保底"手段。
### 自动触发——条件持久化
-通过 `redis.conf` 配置:
+通过 `redis.conf` 配置多条 `save` 规则,Redis 用一个**"脏键计数器"(dirty counter)** 来追踪:
```conf
save 900 1 # 900 秒内至少有 1 个 key 被修改 → 触发快照
@@ -59,227 +67,447 @@ save 300 10 # 300 秒内至少有 10 个 key 被修改
save 60 10000 # 60 秒内至少有 10000 个 key 被修改
```
-> [!QUESTION] 为什么要三档条件?
-> - **低活跃度**:900s/1key 保证最终一致
-> - **中活跃度**:300s/10key 平衡频率和数据量
-> - **高活跃度**:60s/10000key 高频大量变更时快速落盘
+> [!INFO] 底层是怎么工作的?
+> Redis 内部维护两个计数器:
+> 1. **`dirty` 计数器**:自上次 BGSAVE 成功以来,总共被修改的 key 数量(每次写操作 +1)
+> 2. **`lastsave` 时间戳**:上次成功完成 BGSAVE 的 Unix 时间戳
+>
+> 每隔 100ms(`serverCron` 定时任务),Redis 会检查:**当前时间 - lastsave ≥ 阈值时间 && dirty ≥ 阈值次数**,两个条件**同时满足**才会触发 BGSAVE。
+>
+> 所以 `save 900 1` 的意思是:"距离上次快照超过 900 秒**并且**期间有至少 1 个 key 被修改过"。如果 900 秒内没有任何写操作,即使规则匹配了也不会触发——因为没有新数据需要保存。
+
+> [!QUESTION] 为什么要设三档而不是一档?
+> 这是一个**渐进式频率**设计:
+> - **低活跃度**(900s/1key):写入很少时,15 分钟存一次就够了——即使丢了 15 分钟的数据,也几乎没什么损失
+> - **中活跃度**(300s/10key):中等写入压力时,5 分钟存一次,平衡了 I/O 开销和数据安全
+> - **高活跃度**(60s/10000key):大量写入时,1 分钟存一次——虽然 fork 更频繁了,但此时丢 1 分钟的数据损失也更大
+>
+> 本质上是"**数据越重要、变更越频繁,保存就越勤**"的策略。
```bash
-# 运行时动态修改
-CONFIG SET save "900 1\n300 10\n60 10000"
+# 运行时动态修改(覆盖所有规则)
+CONFIG SET save "900 1 300 10 60 10000"
-# 临时关闭自动快照(谨慎!)
+# 临时关闭自动快照(谨慎!相当于禁用 RDB 自动触发)
CONFIG SET save ""
```
-> [!WARNING] CONFIG SET 不持久
-> `CONFIG SET` 仅在内存中生效,重启后失效。修改 `redis.conf` 并 reload 才是持久化的方式。
+> [!WARNING] CONFIG SET 不持久化
+> `CONFIG SET` 仅在内存中生效,Redis 重启后会恢复 `redis.conf` 的配置。若要永久生效,必须修改 `redis.conf` 并执行 `CONFIG REWRITE` 或手动重启。
-### 其他触发场景
+### 其他自动触发场景
-| 场景 | 说明 |
-|------|------|
-| **shutdown 保存** | `SHUTDOWN` / `SHUTDOWN NOSAVE` 会在关闭前执行一次完整 RDB 保存(除非显式传 NOSAVE) |
-| **AOF rewrite** | `BGREWRITEAOF` 重写 AOF 时同样会 fork 子进程生成 RDB preamble(开启混合持久化时) |
-| **主从同步** | 新从节点首次全量同步时,主节点会自动触发 BGSAVE 并将快照传给从节点 |
+除了 `save` 规则外,以下场景也会自动触发 RDB:
-> [!TIP] 避免频繁触发
-> 多个触发条件同时满足时,Redis 会去重,仅执行一次 BGSAVE。若已有 BGSAVE 在执行,后续的 SAVE 请求将被忽略。
+| 场景 | 说明 | 为什么需要? |
+|------|------|-------------|
+| **正常关闭** | 执行 `SHUTDOWN` 时自动触发一次 BGSAVE,保证关闭前数据落盘 | 优雅关闭 ≠ 数据丢失 |
+| **主从全量同步** | 新从节点首次连接主节点时,主节点自动触发 BGSAVE 并将快照传给从节点 | 从节点需要一份完整的数据副本 |
+| **AOF 重写** | 开启混合持久化后,`BGREWRITEAOF` 会先写 RDB 快照作为基线 | 后面"混合持久化"章节详解 |
+
+> [!TIP] Redis 如何避免"撞车"?
+> Redis 同一时刻**只会有一个子进程在做持久化**。如果 BGSAVE 正在执行,此时再触发 SAVE/BGSAVE:
+> - `BGSAVE` → 直接返回错误 `ERR Background save already in progress`
+> - `SAVE` → 会等待 BGSAVE 完成后再执行
+>
+> 这个设计避免了多个子进程争抢 I/O 资源。
## RDB 文件结构
-Redis 7.0+ 引入了新格式,更紧凑且支持增量复制:
+可以把 `.rdb` 文件想象成一个**快递包裹**:有快递单号(Header)、多个包裹(DB 里的数据)、包裹拆完的标记(EOF)、以及签收校验码(Footer)。
```mermaid
flowchart TD
- H1["Header"] --> H2["Magic: REDIS"]
- H1 --> H3["RDB Version"]
- H1 --> H4["Reserved / CRC64"]
+ subgraph header["1 HEADER 文件头"]
+ A["Magic: REDIS07 + Version"]
+ B["Auxiliary Fields: redis-ver, ctime, used-mem"]
+ C["DB Selector: FE 00(选择 DB 0)"]
+ A --> B --> C
+ end
- D1["DB Number
1 byte"] --> D2["Key Type
1 byte"]
- D2 --> D3["Encoded Key + Value"]
+ subgraph data["2 DATA 数据段(每个非空 DB 重复一次)"]
+ D["Key Type: 1 byte(String=0, List=1, Set=2 ...)"]
+ E["Expire Info: 可选, TTL 时间戳"]
+ F["Encoded Key + Value: 直接序列化底层数据结构"]
+ D --> E --> F
+ F -.-> G["... 重复 N 个 Key-Value 对 ..."]
+ end
- E1["EOF Marker"] --> E2["CRC64 Checksum
8 bytes"]
+ subgraph footer["3 FOOTER 文件尾"]
+ H["EOF Marker: 0xFF(数据结束)"]
+ I["CRC64 Checksum: 8 bytes(完整性校验)"]
+ H --> I
+ end
- H1 & H2 & H3 & H4 ==> "Dataset Section"
- D1 & D2 & D3 ==> "Dataset Section"
- D3 -.-> "... N keys ..."
- E1 & E2 ==> "Footer"
+ header --> data --> footer
```
-### 各段含义
+### 各段含义详解
-| 段落 | 内容 | 说明 |
+**1️⃣ Header(文件头)**
+
+- **Magic 字符串**:以 `REDIS` 开头 + 4 字节版本号(如 `REDIS0009`),用于标识"这是一个 RDB 文件"
+- **辅助字段(Auxiliary Fields)**:记录 Redis 版本、创建时间、内存占用等元信息——**不是数据本身,只是"快递单信息"**
+- **DB Selector**:`FE` + DB 编号,告诉你"接下来的数据属于哪个 DB"(默认只有 DB 0 有数据,所以通常只出现一次)
+
+**2️⃣ Data(数据段)**
+
+每个 key-value 对的存储格式:
+
+| 字段 | 大小 | 说明 |
|------|------|------|
-| **Header** | Magic + Version + 校验 | `REDIS` 魔数标识文件类型;版本号用于兼容性判断;CRC64 用于完整性校验 |
-| **DB Selector** | 数据库编号 | 默认 16 个 DB(0-15),只序列化非空 DB |
-| **Key-Value Pairs** | 类型 + 编码 + 数据 | 每个 key 携带类型标记(String/List/Hash/ZSet…),底层编码直接序列化,因此文件极紧凑 |
-| **EOF** | `0xFF` 终止符 | 标识数据段结束 |
-| **Footer** | CRC64 校验和 | 8 字节,启动加载时校验文件完整性 |
+| **Key Type** | 1 byte | 值的类型标记:String=0, List=1, Set=2, ZSet=3, Hash=4 … |
+| **Expire Time** | 0 或 8 bytes | 如果 key 设置了过期时间,会附带一个毫秒级时间戳 |
+| **Key** | 变长 | key 名(字符串编码) |
+| **Value** | 变长 | **直接序列化底层数据结构**(ziplist、intset、skiplist 等),这是 RDB 文件紧凑的关键 |
+
+**3️⃣ Footer(文件尾)**
+
+- **EOF 标记**:`0xFF`,一个字节,宣告"数据到这里结束了"
+- **CRC64 校验和**:8 字节,Redis 加载文件时用它校验完整性——如果校验失败,说明文件损坏,加载直接报错
> [!QUESTION] 为什么 RDB 文件比 AOF 小很多?
-> 因为 RDB 直接序列化底层数据结构(如 ziplist、intset),而非记录产生这些数据的命令字符串。一个 Hash 用 ziplist 存储可能只有几十字节,但生成它的 HSET 命令可能有数百字节。
+> 核心在于存储方式不同。举个例子:
+>
+> 假设你用 `HSET user:1 name "Alice" age 25 city "Beijing"` 存了一个 Hash:
+> - **AOF 记录的是**:这条命令字符串本身 → `*8\r\n$5\r\nHSET\r\n$6\r\nuser:1\r\n$4\r\nname\r\n...\` 约 **120+ 字节**
+> - **RDB 记录的是**:Hash 类型标记 + ziplist 编码的紧凑字节流 → 约 **30-40 字节**
+>
+> 差距来自:RDB 不记录"怎么做的",只记录"结果是什么"——就像一张照片远比描述照片的文字更紧凑。
## 关键配置参数
```conf
# ---- RDB 核心参数 ----
-dbfilename dump.rdb # 快照文件名
-dir /var/lib/redis # 工作目录,RDB/AOF 文件存放位置
-rdbcompression yes # 是否使用 LZF 压缩字符串(推荐开启,CPU vs 磁盘权衡)
-rdbchecksum yes # 是否在文件末尾写入 CRC64 校验(推荐开启)
-rdb-save-incremental-fsync yes # 写入磁盘时增量 fsync,避免一次性大量 I/O 阻塞
-rdb-del-sync-files no # 无盘复制时,是否删除用于同步的临时 RDB(Redis 7+)
+dbfilename dump.rdb # 快照文件名(建议保留默认)
+dir /var/lib/redis # 工作目录——RDB/AOF 文件都存放在这里
+rdbcompression yes # 是否使用 LZF 压缩字符串
+rdbchecksum yes # 是否在文件末尾写入 CRC64 校验
+rdb-save-incremental-fsync yes # 写入磁盘时是否增量 fsync
+rdb-del-sync-files no # 无盘复制时,是否删除用于同步的临时 RDB(Redis 7+)
```
-> [!TIP] rdbcompression 的取舍
-> 开启压缩可以显著减少磁盘占用(通常压缩 40%-60%),代价是写入时多消耗一点 CPU。对于绝大多数场景,**保持 yes** 是最优解。只有在 CPU 极度敏感、磁盘充裕的特殊场景下才考虑关闭。
+### 参数逐个解释
+
+| 参数 | 推荐值 | 为什么? |
+|------|--------|----------|
+| `dbfilename` | `dump.rdb` | 保持默认即可。如果一台机器跑多个 Redis 实例,建议用端口号区分,如 `dump-6379.rdb` |
+| `dir` | `/var/lib/redis` | 快照文件存放目录。**必须确保该目录有足够磁盘空间**,且 Redis 进程有写权限 |
+| `rdbcompression` | `yes` | 开启 LZF 压缩可减少 40%-60% 的磁盘占用,代价是写入时多消耗一点 CPU。**绝大多数场景推荐开启** |
+| `rdbchecksum` | `yes` | 开启后在文件末尾写入 CRC64 校验码,加载时会验证完整性。**关闭可略微提升加载速度,但不值得冒险** |
+| `rdb-save-incremental-fsync` | `yes` | 开启后,子进程每写入 32MB 就执行一次 `fsync`,避免一次性刷盘造成 I/O 尖峰。对 SSD 尤其友好 |
+
+> [!QUESTION] rdbcompression 关闭能提升多少性能?
+> 压缩的 CPU 开销其实很小(LZF 是轻量压缩算法),瓶颈通常在磁盘 I/O 而非 CPU。关闭压缩后文件变大 → 写入时间更长 → 可能反而更慢。所以**除非你有明确的 CPU profiling 数据证明压缩是瓶颈,否则不要关**。
## Fork 过程详解
+BGSAVE 的核心操作是 `fork()`——这是 Linux 的系统调用,能以**极低的成本**创建一份进程副本。整个流程如下:
+
```mermaid
sequenceDiagram
- participant Server as Redis Server
- participant Parent as 主进程
- child ForkGroup as Forked Child
- participant Child as 写时复制子进程
- participant Disk as 磁盘 (.rdb)
- end
+ participant Client as 客户端
+ participant Main as 主进程
+ participant Child as 子进程
+ participant Disk as 磁盘
- Server->>Parent: BGSAVE 指令
- Parent->>Server: OK(立即返回)
- Parent->>Child: fork() 创建子进程
- Note over Child: 子进程独享父进程
内存副本(写时复制 COW)
- Child->>Disk: 遍历所有 key → 序列化
- Note over Child: 主进程仍可读写
COW 机制按需拷贝脏页
- Child->>Parent: 生成临时 .rdb.tmp
- Parent->>Disk: RENAME 原子替换
- Parent->>Server: BGSAVE 完成通知
+ Client->>Main: 发送 BGSAVE 命令
+ Main->>Main: 创建子进程(fork)
+ Note over Main: fork 返回 0(子进程视角)
fork 返回 pid(主进程视角)
+ Main->>Client: 返回 OK(立即响应)
+ par 主进程继续服务
+ Main->>Main: 正常处理读写请求
+ and 子进程执行快照
+ Child->>Child: 遍历所有 key-value
+ Child->>Disk: 序列化写入 dump.rdb.tmp
+ Note over Child,Disk: 写入过程中如果主进程修改了数据
内核通过 COW 机制保护子进程视图
+ Child->>Main: 通知:快照完成
+ end
+ Main->>Disk: rename dump.rdb.tmp -> dump.rdb(原子替换)
+ Main->>Main: 更新 lastsave 时间戳, 重置 dirty 计数器
```
-> [!WARNING] COW 内存消耗
-> fork 后主进程若持续写入大值,会导致 COW 副本膨胀。例如你有一个 1GB 的值被频繁修改,fork 后每次写都可能多拷贝一页(4KB)。高峰期建议关闭自动 RDB,仅依赖 AOF。
+> [!TIP] 为什么 fork 这么快?
+> `fork()` 并不会真的复制整个内存。Linux 使用**"写时复制"(Copy-On-Write, COW)**——fork 之后,父子进程**共享同一份物理内存页**,只在某一方**写入时**才真正拷贝那一页。所以 fork 一个 10GB 的 Redis 实例,实际瞬间开销只是创建页表(通常几十 MB),远小于 10GB。
### 深入理解 COW(Copy-On-Write)
-fork 后父子进程共享同一份物理内存页(只读),只有当某一方**写入**时,内核才会真正复制该页——这就是"写时复制"。
+> [!INFO] 图书馆类比
+> 想象一个图书馆(物理内存),里面有很多书(内存页)。
+> - **fork 之前**:只有主进程一个人在看书
+> - **fork 之后**:子进程也进了图书馆,两人**看同一本书**,不需要额外复印——这就是"共享"
+> - **主进程想在书上做笔记(写入)**:图书馆员说"不行,这本书要保护",于是**复印了一份**给主进程,子进程继续看原版——这就是"写时复制"
+> - **如果主进程不写**:从头到尾只有那一本书,零额外开销
```mermaid
flowchart TD
F["Fork 完成"] --> S["父子进程共享
所有物理内存页"]
S --> W{"主进程收到
写请求?"}
- W -->|"否"| R["子进程继续读取
(零拷贝, 极快)"]
+ W -->|"否"| R["子进程继续读取原页
(零拷贝,极快)"]
W -->|"是"| C["内核复制该内存页
(Copy-On-Write)"]
- C --> N["主进程写入副本
子进程仍读原页"]
+ C --> N["主进程写入副本页
子进程仍读原页"]
R & N --> D["子进程完成序列化"]
- D --> DONE["RDB 文件生成"]
+ D --> DONE["RDB 文件生成完成"]
```
-> [!QUESTION] 为什么 Redis 占用内存会翻倍?
-> 这是面试高频题。答案是:**不会翻倍,但最坏情况接近翻倍。** fork 本身不复制内存,只有**主进程实际写入的页面**才会被 COW。如果 BGSAVE 期间主进程几乎不写入,额外内存开销几乎为零。但若全量写入,就会逐步逼近 2 倍。
+> [!QUESTION] BGSAVE 期间 Redis 内存会翻倍吗?
+> **不会翻倍,但最坏情况接近翻倍。**
>
-> **运维经验**:保持系统 `vm.overcommit_memory=1`(Linux 默认),否则 fork 可能因内核认为内存不足而失败。
+> fork 本身**不复制任何内存页**。只有当主进程**实际写入**某一页时,那一页才会被 COW 拷贝。两种极端情况:
+>
+> | 场景 | 额外内存开销 |
+> |------|-------------|
+> | BGSAVE 期间主进程**几乎不写入** | ≈ 0(只共享,不拷贝) |
+> | BGSAVE 期间主进程**全量写入** | ≈ 接近翻倍(每一页都被拷贝) |
+> | 实际生产环境 | 通常 **10%-30%**(只有被修改的 key 对应的页才会 COW) |
+
+> [!WARNING] fork 失败的常见原因
+> fork 需要内核分配页表空间,如果系统内存不足会失败。关键运维参数:
+> - **`vm.overcommit_memory = 1`**:告诉内核"允许过度提交",这是 Redis 官方推荐的 Linux 设置
+> - **`vm.overcommit_ratio`**:默认 50,配合 overcommit_memory=2 使用
+>
+> 如果你看到 `Can't save in background: fork: Cannot allocate memory`,大概率是这个参数没设置对。
### TTL / 过期键处理
> [!QUESTION] RDB 快照会保存已过期的 key 吗?
-> **不会。** Redis 在 fork 子进程前会先执行一次主动过期扫描,清理掉所有已超时的 key。
+> **不会。** 这是一个容易误解的点——很多人以为快照是"拍照片",会原封不动地保存内存里所有东西。但实际上,Redis 在"拍照"之前会先"打扫房间"。
具体流程:
-1. `BGSAVE` 触发时,主线程先将当前所有已过期 key 从内存中删除
-2. 子进程 fork 完成后遍历剩余 key → 序列化到 .rdb 文件
-3. 因此:**RDB 文件中不会出现已过期键**
+1. `BGSAVE` 触发时,主线程**先执行一次主动过期扫描**,把所有已超时的 key 从内存中删除
+2. 删除完成后,才 fork 子进程
+3. 子进程遍历剩余的 key → 序列化写入 `.rdb` 文件
+4. 结论:**`.rdb` 文件中只有有效数据,不包含过期键**
```mermaid
flowchart LR
- A["BGSAVE 触发"] --> B["主线程: 主动过期
清理超时 Key"]
+ A["BGSAVE 触发"] --> B["主线程: 主动过期扫描
清理所有已超时 Key"]
B --> C["Fork 子进程"]
- C --> D["子进程: 遍历剩余数据
序列化到 .rdb"]
- D --> E[".rdb 无过期键"]
-
- style E fill:"#c4e8b0"
+ C --> D["子进程: 遍历有效数据
序列化写入 .rdb"]
+ D --> E["RDB 文件中无过期键"]
+ style E fill:#c4e8b0
```
-> [!NOTE] 过期策略差异
-> 虽然 RDB 不保存过期键,但 AOF 可能包含过期命令(如之前的 SET k v EX 100)。AOF 重写时会主动过滤过期键,恢复时也只会加载有效数据。两者优先保证启动时的数据一致性。
+> [!NOTE] 一个细节:RDB 加载时也会二次检查
+> 即使某个 key 在快照时还没过期,但等到恢复加载时已经过期了——Redis 在加载 RDB 文件时会**再次检查 TTL**,过期的 key 不会被加载到内存。这保证了无论什么时候恢复,数据都是"新鲜"的。
## 恢复流程
+Redis 启动时会**自动检测** `dir` 目录下的 `.rdb` 文件并加载——你不需要手动执行任何"恢复命令"。
+
+### 场景一:正常重启(最常见)
+
```bash
-# 1. 停止 Redis
-redis-cli SHUTDOWN NOSAVE
+# 直接重启即可,Redis 自动加载 dump.rdb
+redis-server /path/to/redis.conf
+```
-# 2. 把 .rdb 放到 redis_dir(由 dir 配置决定)
-cp dump.rdb /var/lib/redis/
+Redis 启动日志中你会看到类似:
+```
+* DB loaded from disk: 0.052 seconds # ← 52ms 加载完成
+```
-# 3. 启动 Redis
-redis-server --daemonize yes
+### 场景二:从备份恢复(灾难恢复)
+
+当服务器故障需要从备份 `.rdb` 恢复时:
+
+```bash
+# 1. 停止当前 Redis(如果还在运行)
+redis-cli SHUTDOWN NOSAVE # NOSAVE = 不再生成新快照,直接关闭
+
+# 2. 把备份的 .rdb 文件放到 Redis 工作目录
+cp /backup/dump.rdb /var/lib/redis/dump.rdb
+
+# 3. 启动 Redis,它会自动加载这个 .rdb
+redis-server /path/to/redis.conf
# 4. 验证数据恢复
-redis-cli DBSIZE
-redis-cli KEYS "*"
+redis-cli DBSIZE # 检查 key 总数
+redis-cli INFO keyspace # 查看各 DB 的 key 数量和过期 key 数量
+redis-cli GET some_known_key # 抽样验证已知 key 是否存在
```
+### 场景三:迁移数据到新实例
+
+```bash
+# 在旧实例上手动触发一次快照
+redis-cli BGSAVE
+
+# 等待完成
+redis-cli LASTSAVE # 观察时间戳变化
+
+# 复制 .rdb 到新服务器
+scp /var/lib/redis/dump.rdb user@new-server:/var/lib/redis/
+
+# 在新服务器上启动 Redis(自动加载)
+ssh user@new-server "redis-server /path/to/redis.conf"
+```
+
+> [!WARNING] 恢复时的常见坑
+> 1. **文件权限**:确保 Redis 进程对 `dir` 目录有**读写权限**(启动时需要读取,运行时需要写入新快照)
+> 2. **磁盘空间**:`.rdb` 文件恢复时需要临时解压到内存,确保服务器**可用内存 ≥ RDB 文件大小**
+> 3. **同时存在 AOF 和 RDB**:如果 `dir` 下同时有 `dump.rdb` 和 `appendonly.aof`,**Redis 优先加载 AOF**(因为 AOF 数据通常更新)。如果你确实想用 RDB 恢复,需要先移走 AOF 文件
+> 4. **版本兼容**:低版本 Redis 不能加载高版本生成的 RDB 文件(向后不兼容)
+
## RDB vs AOF 对比
-| 维度 | RDB | AOF |
-|------|-----|-----|
-| 数据安全性 | 两次快照间数据丢失 | 每秒/每次可配置,接近零丢失 |
-| 恢复速度 | ⚡ 极快(直接加载二进制文件) | 🐢 较慢(需要回放日志) |
-| 文件大小 | ✅ 小(紧凑压缩) | ❌ 大(文本命令日志) |
-| CPU 开销 | 低(偶尔 fork) | 高(持续追加写入) |
-| 适用场景 | 备份、灾难恢复、大数据集冷备 | 对可用性要求高的在线服务 |
-| 重启耗时 | 秒级 | 取决于日志长度(可 pre-fork) |
+| 维度 | RDB | AOF | 为什么? |
+|------|-----|-----|----------|
+| **数据安全性** | ⚠️ 可能丢失两次快照间的数据 | ✅ 最多丢 1 秒数据(everysec 策略) | RDB 是"定期拍照",AOF 是"实时写日记" |
+| **恢复速度** | ⚡ 极快(直接加载二进制文件) | 🐢 较慢(逐条回放命令) | 读一本 100 页的书 vs 回放 100 万条操作日志 |
+| **文件大小** | ✅ 小(压缩后的二进制) | ❌ 大(文本命令持续追加) | 照片 vs 逐帧视频——信息密度不同 |
+| **CPU 开销** | 低(偶尔 fork) | 高(每条写命令都要追加) | 定期批量 vs 持续实时的天然差异 |
+| **对写入性能影响** | 极小(fork 子进程异步完成) | 有影响(每次写都要 append) | BGSAVE 不阻塞主线程,AOF fsync 可能短暂阻塞 |
+| **适用场景** | 备份、灾难恢复、主从同步 | 对数据安全要求高的在线服务 | 各取所长 |
-## Redis 7 混合持久化
+> [!QUESTION] 生产环境怎么选?
+> **答案是:不要二选一,两个都开。** Redis 4.0+ 引入的混合持久化(下文详解)完美解决了这个矛盾——RDB 保证恢复速度,AOF 保证数据安全。
-结合 RDB + AOF 的优点:
+## Redis 混合持久化(推荐)
-```conf
-aof-use-rdb-preamble yes
-```
+### 为什么需要混合持久化?
-开启后,AOF rewrite 不再只追加命令日志,而是先写入一份 RDB 快照,再追加后续命令:
+回顾一下两者的痛点:
+
+| 方案 | 优点 | 痛点 |
+|------|------|------|
+| RDB | 恢复快、文件小 | 两次快照间的数据可能丢失 |
+| AOF | 数据几乎不丢 | 恢复慢、文件大 |
+
+> [!QUESTION] 能不能把两者结合起来——用 RDB 加速恢复 + 用 AOF 保证数据安全?
+> 可以!Redis 4.0 引入了**混合持久化(Hybrid Persistence)**,正是为了解决这个矛盾。
+
+### 原理:AOF 文件 = RDB 快照 + 增量日志
+
+开启混合持久化后,AOF **重写**时不再只写命令日志,而是:
```mermaid
flowchart LR
- T1["传统 AOF 重建
逐条命令回放"] -->|"O(N) 慢"| R1["重建耗时: 长"]
+ A["AOF 重写触发"] --> B["先写入一份 RDB 快照
(覆盖重写时刻的全量数据)"]
+ B --> C["再追加重写期间的增量命令"]
+ C --> D["最终 AOF 文件"]
- H1["RDB 二进制快照
全量快速还原"] -->|"秒级加载"| R2["重建耗时: 短"]
- H2["AOF 增量日志
仅保留变更后操作"] -->|"精确到毫秒"| R2
-
- style R1 fill:"#f9d0c4"
- style R2 fill:"#c4e8b0"
+ style D fill:#c4e8b0
```
-> [!TIP] 混合持久化几乎是无脑开启的配置——启动快 + 不丢数据。
+也就是说,生成的 AOF 文件结构变成了:
+
+```
+┌──────────────────────────────────────┐
+│ RDB 二进制快照(全量数据基线) │ ← 启动时直接加载,秒级完成
+├──────────────────────────────────────┤
+│ 增量 AOF 命令(快照之后的写操作) │ ← 逐条回放,保证数据不丢
+└──────────────────────────────────────┘
+```
+
+> [!INFO] 用"写日记"来类比
+> - **纯 RDB** = 定期拍照(恢复快,但照片之间的事不知道)
+> - **纯 AOF** = 每天写日记(信息完整,但要从头读到尾才能恢复全部记忆)
+> - **混合持久化** = 定期拍照 + 每天写日记。恢复时先看最近的照片(RDB 部分),再读照片之后的日记(AOF 部分)——既快又完整
+
+### 如何开启
+
+```conf
+# redis.conf
+aof-use-rdb-preamble yes # Redis 4.0+, 默认就是 yes
+appendonly yes # 必须开启 AOF
+```
+
+### 恢复速度对比
+
+```mermaid
+flowchart LR
+ T1["传统 AOF 恢复
回放数百万条命令"] -->|"O(N) 慢"| R1["耗时: 分钟级别"]
+
+ H1["RDB 快照加载
秒级还原全量数据"] -->|"O(1) 快"| R2["耗时: 秒级"]
+ H2["回放少量增量 AOF
仅重写后的命令"] -->|"极少"| R2
+
+ style R1 fill:#f9d0c4
+ style R2 fill:#c4e8b0
+```
+
+> [!TIP] 混合持久化几乎是"无脑开启"的配置
+> - 恢复速度 ≈ RDB(因为大部分数据在 RDB 部分,AOF 增量部分很少)
+> - 数据安全 ≈ AOF(增量日志保证不丢数据)
+> - 开销 = AOF rewrite 的 fork 开销(和纯 AOF 一样)
+>
+> Redis 5.0+ 已默认开启,建议检查你的配置确认一下。
## 最佳实践
> [!SUMMARY] 核心原则:RDB 是备份手段,不是实时数据保障
+> 不要把 RDB 当成"实时安全网"——它的定位是**定期快照 + 灾难恢复兜底**。实时数据安全交给 AOF。
-1. **不要只依赖 RDB**:生产环境推荐 `AOF + RDB` 混合持久化,AOF 保证数据安全性,RDB 保证快速恢复
-2. **合理设置触发频率**:默认三档(900s/300s/60s)适合大部分场景;写入密集型业务可适当提高频率,但注意 fork 开销
-3. **监控 fork 耗时**:`INFO persistence` 中的 `rdb_last_bgsave_time_sec`,超过 1s 就需要关注
-4. **备份策略**:定期将 `.rdb` 文件拷贝到异地存储(S3/OSS),建议保留多个历史版本
-5. **磁盘选择**:RDB 文件写入对磁盘 I/O 有要求,建议使用 SSD 并配合 `rdb-save-incremental-fsync`
-
-> [!WARNING] 大 Key 是 RDB 的天敌
-> 一个 100MB 的 String key,序列化时会导致子进程写入耗时剧增,同时 COW 也可能产生大量页面复制。发现大 Key 后应优先拆分。
-
-## 运维要点
+1. **不要只依赖 RDB**:生产环境推荐 `AOF + 混合持久化`,AOF 保证数据安全,RDB 加速恢复
+2. **合理设置 save 频率**:默认三档适合大部分场景。写入密集型可适当提高频率,但要注意:
+ - 频率越高 → fork 越频繁 → CPU 和内存压力越大
+ - 经验法则:`rdb_last_bgsave_time_sec` 超过 **1 秒**就需要关注
+3. **备份策略(自动化)**:
```bash
-# 监控 RDB 相关指标
+#!/bin/bash
+# redis-backup.sh — 每日定时备份脚本示例
+REDIS_CLI="redis-cli"
+BACKUP_DIR="/backup/redis"
+DATE=$(date +%Y%m%d_%H%M%S)
+
+# 1. 触发一次快照
+$REDIS_CLI BGSAVE
+
+# 2. 等待完成(轮询 lastsave)
+LAST=$($REDIS_CLI LASTSAVE)
+while [ "$($REDIS_CLI LASTSAVE)" == "$LAST" ]; do sleep 1; done
+
+# 3. 拷贝并压缩
+cp /var/lib/redis/dump.rdb "$BACKUP_DIR/dump-$DATE.rdb"
+gzip "$BACKUP_DIR/dump-$DATE.rdb"
+
+# 4. 保留最近 7 天的备份,删除更早的
+find "$BACKUP_DIR" -name "dump-*.rdb.gz" -mtime +7 -delete
+
+echo "Backup completed: dump-$DATE.rdb.gz"
+```
+
+4. **监控关键指标**:通过 `INFO persistence` 关注以下字段:
+
+| 指标 | 含义 | 告警阈值建议 |
+|------|------|-------------|
+| `rdb_last_bgsave_status` | 上次 BGSAVE 是否成功 | ≠ `ok` 立即告警 |
+| `rdb_last_bgsave_time_sec` | 上次 BGSAVE 耗时 | > 1s 关注,> 5s 告警 |
+| `rdb_last_save_time` | 上次成功快照的时间戳 | 距今 > 1h 未触发则排查 |
+| `rdb_changes_since_last_save` | 上次快照后的变更 key 数 | 持续增长说明 save 规则可能失效 |
+
+5. **磁盘选择**:RDB 写入对 I/O 有要求,建议使用 SSD 并保持 `rdb-save-incremental-fsync yes`
+
+> [!WARNING] 大 Key 是 RDB 的天敌
+> 一个 100MB 的 String key,序列化时子进程写入耗时剧增(单个 key 可能就要写几秒),同时 COW 也会产生大量页面复制。
+>
+> **排查方法**:`redis-cli --bigkeys` 扫描大 key,发现后优先拆分。详见 [[hhs/Redis/11-运维与性能调优]]。
+
+## 运维速查手册
+
+```bash
+# ---- 查看 RDB 状态 ----
INFO persistence
-# rdb_last_bgsave_status: OK
-# rdb_last_save_time: 1715000000 ← 最后成功时间戳
+# 重点关注这些字段:
+# rdb_last_bgsave_status: ok ← 上次是否成功
+# rdb_last_bgsave_time_sec: 2 ← 上次耗时几秒
+# rdb_last_save_time: 1715000000 ← 上次成功时间戳(Unix)
+# rdb_changes_since_last_save: 1234 ← 自上次快照后的变更 key 数
-# 检查文件大小
+# ---- 查看快照文件 ----
ls -lh /var/lib/redis/dump.rdb
+# -rw-rw---- 1 redis redis 1.2G May 22 03:00 dump.rdb
-# 压缩备份(已内置 LZ4 压缩,通常不需要额外压缩)
-tar czf redis-backup.tar.gz dump.rdb
+# ---- 手动触发一次快照并等待完成 ----
+redis-cli BGSAVE
+# 等待:轮询 LASTSAVE 直到时间戳变化
+while [ "$(redis-cli LASTSAVE)" == "$OLD" ]; do sleep 0.5; done
+
+# ---- 检查 RDB 文件是否完整(不启动 Redis 的情况下) ----
+# Redis 自带工具,用于检查 .rdb 文件的完整性
+redis-check-rdb /var/lib/redis/dump.rdb
+# 输出 OK 则文件完整,否则会告诉你哪里损坏了
```
## 关联笔记
diff --git a/hhs/Redis/05-AOF持久化.md b/hhs/Redis/05-AOF持久化.md
index a4386a1..04f762e 100644
--- a/hhs/Redis/05-AOF持久化.md
+++ b/hhs/Redis/05-AOF持久化.md
@@ -7,7 +7,12 @@ create time: 2026-05-15 18:13
## 概述
-AOF(Append Only File)以**追加写日志**的方式记录每个写操作。相比 RDB 的快照式恢复,AOF 提供更高的数据安全性——你可以根据策略做到**不丢数据、丢 1 秒、或丢 N 条**。代价是文件更大、恢复更慢。
+AOF(Append Only File)以**追加写日志**的方式记录每个写操作。如果你用过 MySQL 的 binlog 或 Kafka 的 commit log,这个思想是一样的:**不存最终状态,而是存每一步变化过程**。
+
+相比 [[hhs/Redis/04-RDB持久化|RDB]] 的快照式恢复,AOF 提供更高的数据安全性——你可以根据策略做到**不丢数据、丢 1 秒、或丢 N 条**。代价是文件更大、恢复更慢。
+
+> [!question] RDB 已经能持久化了,为什么还需要 AOF?
+> RDB 是「拍照」——每隔 N 秒拍一张快照,两次拍照之间的数据变化全部丢失。AOF 是「录像」——每个写操作都被记录,最多只丢最后一次 fsync 之后的操作。打个比方:RDB 好比每小时存一次文档,AOF 好比开了自动保存。
## 核心机制
@@ -16,19 +21,32 @@ AOF(Append Only File)以**追加写日志**的方式记录每个写操作。
```mermaid
flowchart LR
Client["客户端写入"] --> Server["Redis 主线程
执行命令 + 返回结果"]
- Server --> Buffer["aof_buffer
内核/用户态缓冲区"]
- Buffer --> FSyncPolicy{"刷新策略?"}
+ Server --> Buffer["aof_buf
用户态缓冲区"]
+ Buffer --> FSyncPolicy{"appendfsync 策略?"}
FSyncPolicy -->|everysec| KernelBuf["操作系统页缓存"]
- KernelBuf --> Disk["磁盘 aof 文件"]
+ KernelBuf --> Disk["磁盘 AOF 文件"]
FSyncPolicy -->|always| Disk
- FSyncPolicy -->|no| OSPage["交给 OS 自己 flush"]
+ FSyncPolicy -->|no| OSPage["交给操作系统自行 flush"]
style Server fill:#f9d,stroke:#96c
style Disk fill:#dfd,stroke:#6c6
```
-> [!TIP] 一条命令是怎么落到磁盘的?
-> 1. 客户端发送命令 → 2. Redis 在主线程执行并返回结果 → 3. 同时将命令追加到 `aof_buffer` → 4. 根据 `appendfsync` 策略最终刷入磁盘。整个过程对主线程几乎是异步的。
+> [!tip] 一条命令是怎么落到磁盘的?
+> 1. 客户端发送 `SET mykey hello` → 2. Redis 主线程执行命令、更新内存、返回 OK → 3. **同时**将命令以 RESP 协议格式追加到 `aof_buf`(用户态缓冲区) → 4. 根据 `appendfsync` 策略调用 `fsync()` 最终刷入磁盘
+>
+> 注意步骤 2 和 3 是同步的,但步骤 4 是异步的——这意味着**命令返回成功 ≠ 数据落盘**。
+
+实际落盘的 AOF 文件长这样(RESP 协议格式):
+
+```
+*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$5\r\nhello\r\n
+*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$5\r\nworld\r\n
+*2\r\n$3\r\nDEL\r\n$5\r\nmykey\r\n
+```
+
+> [!question] 为什么 AOF 用 RESP 协议而不是人类可读的纯文本?
+> RESP 是 Redis 自身的通信协议,选择它的原因很简单:**不需要额外的序列化/反序列化开销**,Redis 直接把发给客户端的响应原样写入 AOF,读取时也直接用 RESP 解析器回放。这比发明一套新格式要高效得多。
### 刷新策略配置
@@ -38,18 +56,53 @@ appendfsync everysec # 每秒 fsync <- 推荐(最佳平衡)
# appendfsync no # 完全依赖 OS -> 性能最好,风险最高
```
-| 策略 | 丢失风险 | 吞吐量 (ops/s) | 适用场景 |
-|------|---------|---------------|---------|
-| `always` | 零丢失 | ~900 | 金融级要求(极少用) |
-| `everysec` | 最多丢 1 秒 | ~74k | **生产标准配置** |
-| `no` | OS 决定,可能丢大量 | ~93k | 可接受全量丢失的场景 |
+在理解这三种策略之前,先搞清楚 `fsync` 到底在做什么:
-> [!TIP] everysec 的性能真相
-> `everysec` 不会阻塞主线程——它把 fsync 放到单独的后台线程。即使某个 fsync 花了 2s(磁盘卡顿),也只是这一秒的写入被推迟到下一周期,不会影响主线程。
+> [!info] fsync 的本质:把"快递柜"里的包裹真正送到你手里
+> 当 Redis 调用 `write()` 时,数据只是从用户态拷贝到了**内核页缓存**(类似放进了快递柜),操作系统会择机将其刷到物理磁盘。而 `fsync()` 是强制要求操作系统**立即**把页缓存的数据持久化到磁盘——相当于要求快递柜立即派送。
+>
+> 如果不调用 `fsync()`,数据在页缓存中时如果发生断电,这些数据就丢了。
+
+理解了 fsync,三种策略的区别就一目了然:
+
+| 策略 | fsync 时机 | 丢失风险 | 吞吐量 (ops/s) | 适用场景 |
+|------|-----------|---------|---------------|---------|
+| `always` | 每条写命令后 | **零丢失** | ~900 | 金融级要求(极少用) |
+| `everysec` | 每秒一次(后台线程) | 最多丢 ~1 秒 | ~74k | **生产标准配置** |
+| `no` | 从不主动 fsync,交给 OS | OS 决定,可能丢 30 秒+ | ~93k | 可接受数据丢失的场景 |
+
+> [!warning] everysec "最多丢 1 秒" 是有前提条件的
+> 这个说法成立的前提是 `fsync()` 能在 1 秒内完成。如果磁盘 IO 阻塞(比如同一块盘上跑着其他 IO 密集任务),一次 `fsync()` 可能卡住几秒甚至十几秒——此时未 fsync 的数据量会持续累积,丢失窗口也随之扩大。不过好在 Redis 的 `everysec` 模式下,`fsync()` 在**后台线程**执行,不会阻塞主线程处理新请求。
+
+> [!tip] 为什么 always 这么慢?
+> `always` 是在**主线程**中同步调用 `fsync()`。一次 `fsync()` 耗时约 0.2~2ms(取决于磁盘),这意味着每条写命令都要等磁盘确认,吞吐量直接从万级跌到百级——这就是"数据安全 vs 性能"的经典权衡。
## AOF 重写(Rewrite)
-AOF 文件会不断增长,需要定期重写来去除无效命令(如 key 被 DEL 后又 SET)。
+AOF 文件只会**追加**、永远不会自动清理。时间一长,大量"垃圾命令"会让文件体积失控:
+
+```mermaid
+flowchart LR
+ subgraph AOF_LOG["appendonly.aof(实际内容)"]
+ CMD1["SET counter 0"]
+ CMD2["SET counter 1"]
+ CMD3["SET counter 2"]
+ CMD4["INCR counter"]
+ CMD5["DEL temp_key"]
+ CMD6["SET temp_key abc"]
+ CMD7["DEL temp_key"]
+ end
+ subgraph REALITY["内存中的最终状态"]
+ MEM["counter = 3
temp_key 不存在"]
+ end
+ AOF_LOG -.->|"7 条命令 → 真实状态只有 1 条"| REALITY
+
+ style AOF_LOG fill:#fdd,stroke:#c66
+ style REALITY fill:#dfd,stroke:#090
+```
+
+> [!question] 为什么不直接"删除"过期命令?
+> AOF 是顺序追加的日志文件,就像流水账本——你不能从账本中间撕掉一页而不影响后面所有页码。**只能"重写":把当前内存的真实状态重新写一份新的、精简的 AOF 文件来替换旧文件。**
### 重写触发条件
@@ -71,85 +124,106 @@ auto-aof-rewrite-min-size 64mb # AOF 最小绝对大小(避免刚启动就
### 重写过程(类似 RDB fork)
+重写并不是简单地"删减旧 AOF",而是**从内存中的真实数据出发,生成全新的精简 AOF 文件**。整个过程分三步:
+
```mermaid
sequenceDiagram
- participant S as Redis Server
- participant P as 主进程
- child CG as Rewriter Child
- participant C as 重写子进程
- participant Log as appendonly.aof
- participant NewLog as appendonly.aof_rewrite_tmp
- end
+ participant P as "主进程(继续服务)"
+ participant C as "重写子进程"
+ participant Old as "appendonly.aof(旧)"
+ participant Tmp as "aof_rewrite_tmp(新)"
+ participant Buf as "aof_rewrite_buf"
- S->>P: BGREWRITEAOF
- P->>S: OK
- P->>C: fork() 子进程
- C->>NewLog: fork 时刻起遍历内存 -> 重写为精简命令
- Note over P,C: Main process continues serving requests
- loop 每个新写命令
- P->>Log: 追加原始命令
- P->>C: 同时写入 aof_rewrite_buf
(重写期间的新操作)
- end
- C->>NewLog: 追加 aof_rewrite_buf 中的内容
- C->>P: 完成
- P->>Log: rename(原 -> 旧.bak, 新 -> 当前)
+ P->>P: "收到 BGREWRITEAOF 命令"
+ P->>C: "fork() 创建子进程"
+ Note over C: "第一步:遍历内存所有 key
生成精简命令写入新文件"
+ C->>Tmp: "SET key1 val1
SET key2 val2
HSET hash f1 v1 ..."
+ Note over P: "主进程继续正常处理请求"
+ P->>Old: "新命令照常追加到旧 AOF"
+ P->>Buf: "同时复制一份到重写缓冲区"
+ Note over C: "第二步:内存遍历完成"
+ C->>Buf: "请求主进程发送缓冲区内容"
+ Note over C: "第三步:把重写期间的新命令追加到新文件末尾"
+ Buf->>Tmp: "追加增量命令"
+ C->>P: "重写完成通知主进程"
+ P->>P: "原子 rename:旧文件 → .bak,新文件 → appendonly.aof"
```
-> [!WARNING] 重写的内存开销
-> fork 后重写子进程也使用 COW 机制。如果你的 AOF 正在持续增长且写入量大,可能需要评估可用内存是否足够支撑 fork 的 COW 副本。在 100GB+ 的 Redis 实例上,一次 fork 可能导致额外的几 GB 内存占用。
+> [!tip] 关键理解:为什么需要"重写缓冲区"?
+> 子进程在遍历内存生成新 AOF 的过程中(可能持续几十秒甚至几分钟),主进程还在不断接收新写命令。这些"重写期间的增量命令"被暂存在 `aof_rewrite_buf` 中,等子进程遍历完内存后,再将这些增量追加到新文件末尾——这样才能保证新 AOF 不丢任何数据。
+
+> [!warning] 重写的内存开销
+> fork 后重写子进程使用 COW(Copy-On-Write)机制,和 RDB 的 BGSAVE 一样。如果你的实例内存很大(100GB+)且写入频繁,fork 可能导致额外几 GB 的内存占用。在内存紧张的环境中,建议控制 AOF 文件大小、及时触发重写,避免单次重写数据量过大。
## AOF 开启方式
```conf
-appendonly yes # 开启 AOF
-appendfilename "appendonly.aof" # AOF 文件名
+appendonly yes # 开启 AOF(默认 no)
+appendfilename "appendonly.aof" # AOF 文件名(Redis 7 会在此基础上创建子目录)
dir /var/lib/redis # AOF/RDB 文件存放目录
```
+> [!tip] 在线开启/关闭 AOF
+> 可以通过 `CONFIG SET appendonly yes` 在运行时开启 AOF,但需要注意:这会触发一次阻塞式的 AOF 初始化(类似 BGSAVE),**生产环境建议在低峰期操作**。关闭 AOF 同理:`CONFIG SET appendonly no`。
+
+Redis 7.x 的多文件目录结构:
+
```bash
-# Redis 7.x 目录结构(新版本使用多文件目录而非单文件)
ls /var/lib/redis/appendonlydir/
-total 256M
-drwxr-x--- 2 redis redis 4.0K May 15 18:00 .
-drwxr-x--- 2 redis redis 4.0K May 15 18:00 ..
--rw-r----- 1 redis redis 16K May 15 18:00 manifest.aof
--rw-r----- 1 redis redis 128M May 15 18:00 base.rdb
--rw-r----- 1 redis redis 128M May 15 18:01 incr-0000000000000003.aof
+# total 256M
+# manifest.aof <- 文件清单(记录哪些文件组成完整 AOF)
+# base.rdb <- 最近一次重写生成的 RDB 全量快照
+# incr-0000000000000003.aof <- 增量 AOF(记录重写之后的新写操作)
```
-### 思考:为什么 AOF 会持续增长?
-
-想象以下操作序列:`SET x 1 -> SET x 2 -> SET x 3 -> DEL x`。如果不做处理,AOF 中会记录这 4 条命令,但 key `x` 已经不存在了——这些写入了磁盘却没有任何价值。这就是为什么需要**重写**。
+> [!question] 为什么 Redis 7 要把单文件拆成目录结构?
+> 旧版单个 AOF 文件有一个痛点:重写时需要先生成临时文件,再 `rename` 替换。如果 AOF 有 50GB,rename 操作虽然原子但磁盘 IO 开销巨大。拆成多文件后,重写只需替换 `base.rdb` 和新增 `incr-*.aof`,IO 开销大幅降低,也更方便做增量备份。
## 混合持久化(Hybrid Persistence)
-混合持久化是 Redis 4.0 引入、Redis 7 默认开启的特性——在 AOF 重写时,先用 RDB 快照全量备份当前内存状态,再用 AOF 追加增量变化。
+混合持久化是 Redis 4.0 引入、Redis 7 默认开启的特性,它解决了 AOF 重写的一个尴尬问题:**重写时到底该生成 RDB 还是 AOF?**
+
+> [!question] 纯 AOF 重写的痛点
+> 纯 AOF 重写需要把每个 key 的当前状态都转成 SET/HSET 等命令写入文件。当实例有几十万个 key 时,这些命令序列化+解析的过程会显著拖慢恢复速度。而 RDB 是二进制格式,加载速度比逐条执行 AOF 命令快 10~100 倍。
+>
+> 混合持久化的答案很简单:**两种都用**。重写时先写 RDB 全量快照作为"底子",再追加一小段 AOF 增量命令作为"补丁"。
```conf
# redis.conf
aof-use-rdb-preamble yes # 开启混合持久化(默认值)
```
-```mermaid
-flowchart LR
- BG["BGREWRITEAOF"] --> Fork["fork 子进程"]
- Fork --> RDBPart["RDB 全量部分
二进制快照,写入极快"]
- Fork --> AOFPart["AOF 增量部分
仅含 fork 后的写操作"]
- RDBPart --> Merge["合并为临时文件"]
- AOFPart --> Merge
- Merge --> Rename["原子 rename 替换"]
+重写后生成的文件结构如下:
- style RDBPart fill:#bbf,stroke:#66c
- style AOFPart fill:#dfd,stroke:#090
+```mermaid
+flowchart TD
+ subgraph NEW_AOF["重写后的新 AOF 文件"]
+ direction LR
+ RDB["RDB 二进制块
(全量数据快照)"] --> AOF["AOF 增量块
(仅重写期间的新写操作)"]
+ end
+
+ Fork["fork 子进程"] --> RDB
+ Fork --> AOF
+ RDB --> Merge["合并写入临时文件"]
+ AOF --> Merge
+ Merge --> Rename["原子 rename 替换旧文件"]
+
+ style RDB fill:#bbf,stroke:#66c
+ style AOF fill:#dfd,stroke:#090
style Merge fill:#ffd,stroke:#cc0
```
**好处有两个:**
-1. **恢复速度快**:启动时先加载 RDB 全量快照(二进制格式比解析 AOF 命令快 10~100 倍),再回放少量 AOF 增量命令
-2. **文件体积小**:相比纯 AOF 记录每条操作指令,混合模式下文件通常缩小 70%~90%
+1. **恢复速度快**:启动时先加载 RDB 二进制快照(顺序读、无需解析命令语法),再回放少量 AOF 增量命令。即使 AOF 增量段有几十 MB,相比从头解析几十 GB 的纯 AOF 命令也快得多
+2. **文件体积小**:RDB 本身就是压缩的二进制格式,比等量数据的纯 AOF 文本命令小 70%~90%
-> [!WARNING] 恢复流程的变化
-> 混合模式下,Redis 启动时先读取 `base.rdb` 还原全部数据,再逐条执行增量 `.aof` 段中的命令。如果只保留 AOF 段而删除 `base.rdb`,恢复将退化为纯 AOF 模式,速度大幅下降。
+> [!warning] Redis 7 的多文件目录结构
+> Redis 7 不再使用单个 AOF 文件,而是采用多文件目录结构:
+> - `base.rdb`:AOF 重写时生成的 RDB 全量快照
+> - `incr-*.aof`:增量 AOF 文件(记录重写之后的新写操作)
+> - `manifest.aof`:文件清单,记录哪些文件属于同一个 AOF 实例
+>
+> 恢复时 Redis 会按 manifest 加载:先加载 `base.rdb`,再按顺序回放所有 `incr-*.aof`。如果删除了 `base.rdb`,恢复退化为纯 AOF 模式,速度会大幅下降。
## AOF vs RDB 对比
@@ -167,29 +241,48 @@ flowchart LR
## AOF 故障恢复
+断电或异常退出时,AOF 文件可能出现**尾部截断**(最后一条命令不完整)。Redis 启动时会自动检测并尝试修复。
+
+### 常见故障场景与修复
+
```bash
-# 如果 AOF 损坏(断电等原因导致截断)
-redis-server --repair appendonly.aof
-# 或 Redis 7 下修复整个目录
-redis-server --fix appendonlydir/aof-manifest
+# 场景一:AOF 文件末尾被截断(最常见——断电导致最后一条命令写了一半)
+# Redis 启动时会自动丢弃末尾不完整的命令,无需手动干预
+
+# 场景二:AOF 文件损坏严重(如磁盘坏块导致文件中间损坏)
+# 需要手动修复:找到最后一个合法命令,截断之后的内容
+redis-check-aof --fix appendonly.aof
+
+# 场景三:Redis 7 多文件目录结构
+redis-check-aof --fix appendonlydir/appendonly.aof.1.incr.aof
```
+> [!tip] 实际修复流程
+> 1. 先备份损坏的 AOF 文件:`cp appendonly.aof appendonly.aof.bak`
+> 2. 运行 `redis-check-aof --fix`(交互式,会告诉你损坏位置并确认是否截断)
+> 3. 重新启动 Redis,检查数据完整性
+> 4. 如果数据缺失严重,从 RDB 冷备份恢复 + 回放最近的增量 AOF
+
### fsync 失败时的行为
-```
-fsync -> EIO(磁盘错误)
- ↓
-ERROR "I/O error writing to APPEND ONLY FILE..."
- ↓
-立即 SHUTDOWN NOSAVE(停止服务防止数据进一步损坏)
+```mermaid
+flowchart TD
+ FS["fsync() 调用"] --> OK{"返回成功?"}
+ OK -->|"是"| CONT["继续正常服务"]
+ OK -->|"否:EIO 磁盘错误"| LOG["记录日志
I/O error writing to APPEND ONLY FILE"]
+ LOG --> SD["执行 SHUTDOWN NOSAVE
立即停止服务"]
+
+ style SD fill:#fdd,stroke:#c00
+ style CONT fill:#dfd,stroke:#090
```
-> [!NOTE] Redis 为什么会主动 shutdown?
-> 这是一种「fail-fast」设计哲学。AOF 的核心价值就是数据安全,当底层磁盘出现问题时,继续运行只会导致更多数据静默损坏。宁可停机也不能让用户在「不确定」的数据上继续业务。
+> [!note] Redis 为什么会主动 shutdown?
+> 这是一种「fail-fast」设计哲学。AOF 的核心价值就是数据安全,当底层磁盘出现问题时,继续运行只会导致更多数据静默损坏。**宁可停机也不能让用户在"不确定"的数据上继续业务**——这和数据库遇到 WAL 写入失败时的处理逻辑是一样的。
-> [!WARNING] AOF 不是万能的
-> - AOF 修复工具只能恢复大部分,极端情况下会有部分命令丢失
-> - 定期做冷备份(RDB 迁移到 OSS/S3)仍然是必须的兜底手段
+> [!warning] AOF 不是万能的
+> - AOF 修复工具只能处理末尾截断,中间损坏只能截断丢失后面的数据
+> - 定期做冷备份(RDB 快照传到 OSS/S3)仍然是必须的兜底手段
+> - 生产环境建议部署**主从架构**:即使主节点 AOF 出问题,从节点仍有完整数据
## AOF vs RDB 选型决策树
diff --git a/hhs/Redis/06-主从与哨兵.md b/hhs/Redis/06-主从与哨兵.md
index b502bf5..501309f 100644
--- a/hhs/Redis/06-主从与哨兵.md
+++ b/hhs/Redis/06-主从与哨兵.md
@@ -7,13 +7,29 @@ create time: 2026-05-15 18:13
## 概述
-在生产环境中,单节点 Redis 存在两大问题:**写入能力有上限**、**宕机即停服**。本节介绍两个构建基础高可用的核心机制——
+### 从单机困境说起
+
+假设你的 Redis 跑在一台服务器上,所有读写都走这一个实例。随着业务增长,两个现实问题会逐渐暴露:
+
+1. **读性能瓶颈**:热点 key 被反复访问,单实例 QPS 顶到天花板(通常 10w 左右),请求开始排队
+2. **单点故障风险**:服务器宕机 = Redis 全线不可用,数据甚至可能丢失
+
+> [!QUESTION] 想一想:如果让你来设计解决方案,你会怎么做?
+> - 瓶颈在"读"→ 能不能让**多个节点分摊读请求**?
+> - 风险在"单点"→ 能不能让数据**自动备份到其他机器**?
+> - 故障了靠人恢复太慢 → 能不能**自动检测并切换**?
+
+这就是 **主从复制** 和 **Sentinel** 要解决的问题。
+
+### 两大核心机制
| 机制 | 解决什么问题 | 一句话说明 |
|------|------------|----------|
| **主从复制(Replication)** | 数据冗余 + 读扩展 | Master 处理写操作,Replica 自动同步数据并承担读请求 |
| **Sentinel(哨兵)** | 人工恢复慢 | 自动检测 Master 故障,提升最佳 Replica 为新 Master |
+通俗地说:主从复制是"抄作业"机制——Master 做完(写入),Replica 抄一份;Sentinel 是"监考老师"——发现 Master"交不了卷"(宕机),立刻指定一个 Replica"接替答题"。
+
> [!SUMMARY] 知识定位
> - ✅ 本方案适合 **读多写少** 的场景(8:2 ~ 9:1)
> - ⚠️ 写入仍然集中在单点,若需水平写入 → [[hhs/Redis/07-集群方案]]
@@ -40,79 +56,120 @@ flowchart TD
### 异步 vs 半同步
-> [!QUESTION] 为什么 Redis 选择异步复制?
-> 如果等 Replica 确认后 Master 才返回客户端,那网络延迟 = 写入延迟。对于追求低延迟的场景(微秒级),这是不可接受的。
+复制的核心矛盾是:**性能** 和 **数据安全** 不可兼得。我们先看两种策略的区别:
-Redis 默认**异步复制**——Master 写完即返回客户端,不等待 Replica 确认。这保证了极致低延迟,代价是极端情况下可能丢失数据:
-
-```text
-场景: 写 A=1 → Master 返回 OK → Master 宕机(还没传到 Replica)→ 重启后 A 不存在
+```mermaid
+flowchart LR
+ subgraph async["异步复制(Redis 默认)"]
+ A1["Master 写入"] -->|"立即返回 OK"| A2["客户端"]
+ A1 -.->|"后台异步发送"| A3["Replica"]
+ end
+ subgraph semisync["半同步(手动启用)"]
+ B1["Master 写入"] -->|"等待 Replica 确认"| B2["Replica"]
+ B2 -->|"确认收到"| B3["客户端收到 OK"]
+ end
```
-若业务需要 **至少一份副本落盘** 的强一致性保证,可配合 Lua + `wait` 命令实现弱半同步:
+> [!QUESTION] 为什么 Redis 默认选择异步复制?
+> 想象一下:你去超市买东西,收银员每收一笔钱都要先打电话给总部确认到账了才给你小票——你愿意等吗?对追求低延迟的 Redis 来说,**等 Replica 确认 = 强制每次写入多等一次网络往返**(通常 0.1~1ms),这对微秒级响应的场景是不可接受的。
+
+**异步复制** 的工作方式:Master 写完就返回客户端,Replica 的事"后台慢慢同步"。代价是极端情况下可能丢数据:
+
+```text
+场景: 写 A=1 → Master 返回 OK → Master 突然宕机(数据还没来得及传到 Replica)
+ → Replica 上没有 A=1 → 用 Replica 接管后,这个写入就丢了
+```
+
+> [!NOTE] 这个丢数据的概率高吗?
+> 正常情况下极低——因为 Master 到 Replica 的同步几乎是实时的(毫秒级),只有在 Master 写入后**立刻宕机**的极短窗口内才会丢。这就像你刚存完钱、银行还没来得及记账就停电了——概率很小,但在金融场景下不能接受。
+
+#### 如果业务要求"至少一份副本确认"呢?
+
+Redis 提供了 `WAIT` 命令,可以让客户端**阻塞等待**指定数量的副本确认同步完成。这不算严格的半同步(因为 Master 已经先写入并返回了),但能大幅降低丢数据的概率:
```go
-// Go: 确保至少 N 个副本成功写入
+// Go: 写入后,确保至少 1 个副本已经同步了这条命令
+client.Set(ctx, "order:12345", orderData, 0)
+
numReplicas := 1 // 至少 1 个 replica 收到
-timeout := 100 // 超时 100ms
-result := client.Do(ctx, "WAIT", numReplicas, timeout).Int()
-// result == 1 → 至少有 1 个副本已同步
+timeout := 100 // 最多等 100ms,超时就放弃
+replicaCount := client.Do(ctx, "WAIT", numReplicas, timeout).Int()
+
+if replicaCount >= 1 {
+ // 放心:就算 Master 立刻宕机,至少有 1 个 Replica 有这份数据
+} else {
+ // 超时了,数据可能只在 Master 上 → 业务层面考虑重试或告警
+}
```
> [!TIP] WAIT 的代价
-> 阻塞当前线程直到满足条件或超时。高频写场景慎用,建议仅在关键事务中使用。
+> `WAIT` 会阻塞当前连接直到满足条件或超时。高频写场景慎用,建议仅在关键业务路径上使用(如订单创建、扣款等),普通缓存写入不需要。
### 全量同步 vs 增量同步
+主从同步分为两种模式,理解它们的前提是知道一个关键概念:**Replication Offset(复制偏移量)**——你可以把它想象成一条数据"流水线"上的**刻度尺**。Master 每写入一条命令,刻度就往后移一点;Replica 同步到哪里了,也有自己的刻度。只要两个刻度对得上,就能"增量同步"。
+
> [!NOTE] PSync:一次连接,多次复用
-> Redis 2.8+ 引入 PSYNC 协议替代旧版 SYNC,核心优势:**断线重连后可增量同步**。
+> Redis 2.8+ 引入 PSYNC 协议替代旧版 SYNC,核心优势:**断线重连后可增量同步**。旧版 SYNC 每次断线都得全量传输(相当于每次请假都得抄一整本笔记),PSYNC 改为"只抄你缺的那几页"。
```mermaid
sequenceDiagram
participant R as Replica
participant M as Master
- Note over R,M: ── 阶段 1: 全量同步(首次或断线过长)──
- R->>M: PSYNC ? -1 (首次连接)
- M->>M: BGSAVE → dump.rdb.tmp
- M-->>R: +CONTENTS dump.rdb
- R->>R: 重写本地数据集
+ Note over R,M: "阶段 1: 全量同步(首次连接或断线过久)"
+ R->>M: PSYNC ? -1 (首次连接, 没有 offset 记录)
+ M->>M: 触发 BGSAVE, 生成 RDB 快照文件
+ M-->>R: +FULLRESYNC , 然后发送 RDB 文件
+ R->>R: 清空旧数据, 用 RDB 全量覆盖
- loop 同步期间的新命令
+ Note over M: "RDB 生成期间, 新的写入命令不能丢"
+ loop "RDB 传输期间的新写入"
M->>M: 追加到 repl-backlog-buffer
end
- M-->>R: +CONTENTS buf (缓冲命令流)
- R->>R: 重放缓冲命令
+ M-->>R: 发送 backlog 中缓存的命令流
+ R->>R: 重放缓冲命令, 追上 Master 的进度
- Note over R,M: ── 阶段 2: 增量同步(后续心跳)──
- loop 通常 1 秒一次
- R->>M: PSync
- M->>R: 发送 offset 之后的命令
+ Note over R,M: "阶段 2: 增量同步(后续心跳, 每秒一次)"
+ loop "正常运行期间"
+ R->>M: PSYNC
+ M->>R: 只发送 offset 之后的增量命令
end
```
+> [!QUESTION] 全量同步的代价是什么?
+> 全量同步需要 Master 执行 `BGSAVE`(fork 子进程生成 RDB 文件),然后通过网络传给 Replica。如果数据量大(比如 10GB),这个过程会**消耗大量 CPU、内存和网络带宽**。所以我们的目标是:**尽量避免全量同步,让增量同步成为常态**。
+
#### 什么情况下触发全量同步?
-| 触发条件 | 说明 |
-|---------|------|
-| 首次连接 | Replica 为空,必须下载完整 RDB |
-| Master 重启 | Run ID 改变,无法增量恢复 |
-| Replica 手动 SLAVEOF | 主动请求全量同步 |
-| Backlog 过小 | 断线时间超出 repl-backlog-size 缓存容量 |
-| config-resetstat 执行 | 元数据重置导致无法增量 |
+| 触发条件 | 为什么? | 能避免吗? |
+|---------|------|------|
+| **首次连接** | Replica 是空的,没有数据也没有 offset,只能全量下载 | ❌ 必然发生 |
+| **Master 重启** | Master 重启后 Run ID 变了,Replica 认不出"老朋友" | ⚠️ 持久化可缓解(重启后 Run ID 不变) |
+| **Replica 手动执行 SLAVEOF** | 相当于主动"认新主人",必须重新来过 | ❌ 手动触发,通常可控 |
+| **Backlog 溢出** | 断线太久,Master 的"缓冲笔记本"已经翻页覆盖了断线前的内容 | ✅ **增大 `repl-backlog-size`** |
+| **config-resetstat 执行** | 元数据被重置,offset 信息丢失 | ⚠️ 少用此命令 |
-### 复制背调(Replication Backlog)
+> [!WARNING] 生产环境最常见的"不必要全量同步"
+> 绝大多数情况是 **Backlog 过小**导致的。网络抖动几秒,Backlog 不够用,Replica 被迫全量同步——这在网络不稳定的环境中会反复发生,严重拖垮 Master 性能。**加大 Backlog 是性价比最高的优化手段**。
-每个 Master 内部维护一个固定大小的环形缓冲区(默认 1MB):
+### 复制背压(Replication Backlog)
+
+每个 Master 内部维护一个固定大小的 **环形缓冲区**(默认仅 1MB),这是实现增量同步的关键:
```mermaid
flowchart LR
- WriteHead["写入指针"] -->|"新命令流入"| Buffer["repl-backlog-buffer
环形缓冲区(默认1MB)"]
- ReadHead["读取指针"] -->|"命令回放给 Replica"--> Buffer
- Buffer -->|"溢出"`覆盖最旧数据`"
+ WriteHead["写入指针(新命令)"] -->|"追加写入"| Buffer["repl-backlog-buffer
环形缓冲区"]
+ Buffer -->|"读取并发送"| ReadHead["读取指针(发给 Replica)"]
+ Buffer -->|"空间不足时"| Overwrite["覆盖最旧数据"]
```
+> [!NOTE] 什么是"环形缓冲区"?
+> 想象一个**圆形的笔记本**,只有固定的 100 页。你从第 1 页开始记笔记,记满了就回到第 1 页覆盖重写。Replica 落后得越多,它需要"回看"的内容就越远。如果它的位置已经被覆盖了,就只能全量重新抄一本——这就是全量同步。
+>
+> 所以 Backlog 的大小直接决定了:**Replica 最多能断线多久还能增量恢复**。
+
```conf
# 相关配置
repl-backlog-size 256mb # 缓冲区大小,增大允许更长断线恢复
@@ -123,68 +180,90 @@ repl-backlog-ttl 3600 # 无人订阅时多久自动释放(秒)
- backlog = 1MB → 约支持 **2 秒** 断线恢复
- backlog = 256MB → 约支持 **8 分钟** 断线恢复
-### 大键分裂问题(Big Key Split)
+### 大 Key 问题
-当某个 key 体积很大时(如几 MB 的 Hash/Set),一次 `SET` 会阻塞整个 Redis 进程数毫秒甚至更久。对 Replica 来说,这个命令在网络上传输也会成为瓶颈。
+> [!QUESTION] 一个 key 有 5MB 大小,会有什么问题?
+> - **写入阻塞**:Redis 是单线程的,写入一个 5MB 的 Hash/Set 会阻塞其他所有请求几毫秒甚至更久
+> - **同步瓶颈**:这条命令不仅要在 Master 上执行,还要通过网络传给每个 Replica,相当于同一条 5MB 的数据在内网里传 N 次
+> - **内存尖峰**:`BGSAVE` 时 fork 子进程,大 key 会导致 COW(Copy-On-Write)产生大量内存拷贝
> [!SUMMARY] 如何发现大 Key?
> ```bash
> redis-cli --bigkeys # 在线扫描(谨慎!高 QPS 环境有性能影响)
-> scan 0 MATCH * COUNT 100000 # 逐页扫描,用 type/hashlist/set 分别评估
+> scan 0 MATCH * COUNT 100000 # 逐页扫描,用 type/hashlist/set 分别评估
> ```
-解决方案:
-- 写入侧:**分拆大 key** 为多个小 key(如按用户 ID 哈希分散)
-- 同步侧:增大 `repl-diskless-sync-delay` 让多个 Replica 同时接收,避免重复传输
+**解决方案**:
+- **写入侧**:拆分大 key 为多个小 key(例如一个存储用户全部订单的 Hash → 按月份拆分为 `user:orders:2026-05`、`user:orders:2026-06` 等多个小 Hash)
+- **同步侧**:启用无盘复制(`repl-diskless-sync yes`),Master 直接通过 socket 把 RDB 发给所有 Replica,避免写磁盘再读磁盘的双重开销
## 二、级联复制(拓扑优化)
-当 Replica 数量增多时,每个都连 Master 会造成带宽压力:
+### 为什么要级联?
+
+假设 Master 需要服务 10 个 Replica,每个 Replica 每秒同步 ~500KB 的命令流,那 Master 每秒要输出 **5MB 的复制流量**。而且每个 Replica 断线重连都可能触发全量同步——Master 的网络带宽和 CPU(BGSAVE 的 fork)会成为瓶颈。
+
+> [!QUESTION] 怎么降低 Master 的压力?
+> 答案:不让所有 Replica 都直接连 Master,而是让部分 Replica "转发"数据给其他 Replica,形成**树形拓扑**。
```mermaid
flowchart LR
- Master["Master"]
- R1["Replica-1
(也接受从连接)"]
+ Master["Master
只服务 2 个 Replica"]
+ R1["Replica-1
(同时作为下游的 Master)"]
R2["Replica-2"]
- R3["Replica-3"]
- R4["Replica-4"]
+ R3["Replica-3
(从 R1 同步)"]
+ R4["Replica-4
(从 R1 同步)"]
- Master --> R1
- Master --> R2
- R1 --> R3
- R1 --> R4
+ Master -->|"直接同步"| R1
+ Master -->|"直接同步"| R2
+ R1 -->|"级联同步"| R3
+ R1 -->|"级联同步"| R4
```
> [!TIP] 最佳实践
-> 只有 1~2 个 Replica 直接连 Master,其余通过级联获取数据。降低 Master 的网络和 CPU 负担。
+> - 只有 1~2 个 Replica 直连 Master,其余通过级联获取数据
+> - 级联的 Replica 也能配置为接受下游连接(Redis 原生支持,无需额外设置)
+> - **缺点**:级联层级越深,末端 Replica 的数据延迟越大。通常 **两层** 是合理的上限
## 三、只读副本注意事项
-```conf
-replica-read-only yes # 默认值
-```
+Replica 默认是 **只读** 的(`replica-read-only yes`),这不是 Redis 故意限制你,而是有充分的技术原因:
-> [!WARNING] 禁止在 Replica 上写操作
-> 即使关闭 `read-only`,Replica 上的写入在主从重新同步时会被清掉。而且 `DEL` 一个不存在的 key 也会触发额外命令发送给 Master。
+> [!WARNING] 为什么不应该在 Replica 上写数据?
+> 想象一下:你在 Replica 上写入了 `SET foo bar`,但 Master 上没有这个写入。下一次同步发生时,Master 会把自己的数据集"覆盖"到 Replica 上——你写的数据**悄无声息地消失了**。更糟糕的是,如果你在 Replica 上 `DEL` 一个不存在的 key,这个命令可能会被传播到 Master,导致 Master 上原本存在的数据被删掉。
+>
+> **结论**:在 Replica 上写数据,本质上是在制造**数据不一致的定时炸弹**。
-### Replica 的其他关键配置
+### Replica 的关键配置详解
```conf
# --- 副本如何发现 Master ---
-replica-announce-ip 192.168.1.20 # 对外宣告的 IP(Docker/NAT 环境下必设)
+# 在 Docker/NAT 环境下,容器内部 IP 和宿主机 IP 不同。
+# 如果不设置 announce-ip,Sentinel 和其他节点可能会记录容器内部 IP,
+# 导致跨网络访问失败。
+replica-announce-ip 192.168.1.20 # 对外宣告的 IP
replica-announce-port 6379 # 对外宣告的端口
# --- Replica 是否可被查询 ---
-replica-lazy-flush no # FULLRESYNC 后清空旧数据策略:no=立即 flush
-replica-serve-stale-data yes # Master 不可达时是否继续服务请求(默认是)
-replica-read-only yes # 只读保护
+# 这组配置决定:当 Replica 和 Master 断开连接时,还能不能对外服务?
+replica-serve-stale-data yes # yes=断线时仍返回旧数据(可用性优先)
+ # no=断线时直接报错(一致性优先)
+replica-read-only yes # 只读保护,防止误写入导致数据不一致
+replica-lazy-flush no # FULLRESYNC 时清空旧数据的策略:
+ # no=立即 flush(默认),yes=惰性清理
# --- 副本同步相关 ---
-repl-diskless-sync no # 无盘复制开关
-repl-diskless-sync-delay 5 # 等待其他副本同时连接的延迟(秒)
-repl-timeout 60 # 同步超时阈值
+repl-diskless-sync no # 无盘复制:Master 不写 RDB 文件,
+ # 直接通过 socket 发给 Replica(适合 SSD 环境)
+repl-diskless-sync-delay 5 # 开启无盘复制后,等几秒让多个 Replica 同时接收
+repl-timeout 60 # 同步超时阈值(秒),超时则断开重连
```
+> [!QUESTION] `replica-serve-stale-data` 应该设为 yes 还是 no?
+> 这取决于你的业务对**可用性 vs 一致性**的取舍:
+> - **电商首页**、**推荐列表**等场景:设为 `yes`,返回稍微过期的数据总比报错好
+> - **库存扣减**、**余额查询**等场景:设为 `no`,宁可不可用也不能返回错误数据
+
## 四、核心参数调优参考
> [!NOTE] 根据业务特点选择合适的参数组合
@@ -222,8 +301,23 @@ redis-cli INFO replication
## 五、Sentinel 哨兵机制
+### 为什么需要 Sentinel?
+
+上文介绍了主从复制,但有一个关键问题没解决:**Master 宕机了怎么办?**
+
+> [!QUESTION] 没有 Sentinel 时,Master 故障的处理流程是什么?
+> 1. 运维人员收到告警(可能是半夜 3 点)
+> 2. 登录服务器,确认 Master 确实挂了
+> 3. 手动挑一个数据最新的 Replica,执行 `SLAVEOF NO ONE` 提升为新 Master
+> 4. 让其他 Replica 指向新 Master
+> 5. 通知客户端切换连接地址
+>
+> 整个过程可能需要 **几分钟甚至更久**,而且容易出错。Sentinel 要做的就是把这整套流程**自动化**。
+
### 架构组成
+Sentinel 本身也是一个 Redis 进程(只是不处理普通数据读写),它的工作就是**盯着 Master 和 Replica,必要时自动故障转移**。
+
```mermaid
flowchart TD
S1["Sentinel-1"]
@@ -233,36 +327,52 @@ flowchart TD
MasterNode["Master"]
RepNode["Replica"]
- S1 <-->|心跳 PING/PONG| S2
- S1 <-->|心跳 PING/PONG| S3
- S2 <-->|心跳 PING/PONG| S3
+ S1 <-->|"互相探测心跳"| S2
+ S1 <-->|"互相探测心跳"| S3
+ S2 <-->|"互相探测心跳"| S3
- S1 <--> MasterNode
- S2 <--> MasterNode
- S3 <--> MasterNode
+ S1 <-->|"监控 Master/Replica 状态"| MasterNode
+ S2 <-->|"监控 Master/Replica 状态"| MasterNode
+ S3 <-->|"监控 Master/Replica 状态"| MasterNode
- MasterNode <--> RepNode
+ MasterNode <-->|"主从同步"| RepNode
```
> [!QUESTION] 为什么需要奇数个 Sentinel?
-> Sentinel 选举采用多数派原则(Quorum)。3 个节点中 2 个达成共识即可;5 个中需要 3 个。偶数增加 1 个 Sentinel 不会带来额外投票优势,反而浪费资源。
+> 故障转移需要**多数派投票**才能决策(和选举类似)。3 个 Sentinel 中有 2 个同意就能执行;但如果只有 2 个,一旦 1 个挂了,剩下的 1 个永远达不到多数,故障转移就无法执行。偶数个(比如 4 个)也浪费——4 个节点允许 1 个故障,和 3 个一样,却多消耗了一份资源。
+>
+> **记住这个公式:N 个 Sentinel 允许 (N-1)/2 个故障**。3 个允许 1 个故障,5 个允许 2 个故障。
### 核心功能
-#### 1. 监控(Monitoring)
+Sentinel 有三大核心职责:**监控**、**通知**和**故障转移**。我们逐一拆解。
-```bash
-# Sentinel 定期向 Master/Replica 发 PING
-# Master 必须响应:PING → PONG 或 INFO
-# Replica 同理
+#### 1. 监控(Monitoring)——"心跳检测"
-# 三种下线判断
-1. SDOWN (Subjectively Down) — 某个 Sentinel 认为不可达
-2. ODOWN (Objectively Down) — Quorum 数量的 Sentinel 都认为不可达
-3. Failover in progress — 进入故障转移流程
+每个 Sentinel 每隔 1 秒向 Master 和 Replica 发送 `PING` 命令,期待收到 `PONG` 回复。如果在 `down-after-milliseconds` 时间内没有收到回复,Sentinel 就认为这个节点"有问题了"。
+
+但一个 Sentinel 的判断可能是误判(比如只是 Sentinel 和 Master 之间的网络抖动),所以需要多个 Sentinel 协同确认:
+
+```mermaid
+flowchart TD
+ A["Sentinel-1 向 Master 发 PING"] --> B{"收到 PONG?"}
+ B -->|"是"| C["一切正常, 继续监控"]
+ B -->|"否"| D["标记 Master 为 SDOWN
(主观下线: 我觉得它挂了)"]
+ D --> E{"询问其他 Sentinel:
你们觉得 Master 还活着吗?"}
+ E -->|"Quorum 个 Sentinel
都认为挂了"| F["标记 Master 为 ODOWN
(客观下线: 大家都确认它挂了)"]
+ E -->|"没达到 Quorum"| G["可能是网络局部问题
保持 SDOWN, 继续观察"]
+ F --> H["进入故障转移流程"]
```
-#### 2. 选主(Leader Election)
+> [!NOTE] SDOWN vs ODOWN,一句话总结
+> - **SDOWN**(Subjectively Down)= "我觉得它挂了"——单个 Sentinel 的主观判断
+> - **ODOWN**(Objectively Down)= "大家都确认它挂了"——达到 Quorum 个 Sentinel 的客观共识
+>
+> 只有 ODOWN 才会触发真正的故障转移,SDOWN 只是一个"预警"状态。
+
+#### 2. 选主(Leader Election)——"谁来操刀故障转移"
+
+当 Master 被判定 ODOWN 后,**不能所有 Sentinel 都去执行故障转移**(否则会乱套)。需要先选举一个 **Sentinel Leader** 来统一操刀:
```mermaid
sequenceDiagram
@@ -270,98 +380,170 @@ sequenceDiagram
participant S2 as Sentinel-2
participant S3 as Sentinel-3
- S1->>S2: 提议自己为 leader (SEND ME YOUR VOTE)
- S2->>S1: OK (投票给 S1)
- S3->>S1: OK (投票给 S1)
- S1->>S2: 我当选 leader
- Note over S1,S3: ⚡ 任意 Sentinel 都可以主动发起选举
+ Note over S1,S3: "Master 被判定 ODOWN, 需要选出一个 Leader"
+ S1->>S2: "我要当 Leader, 请投我一票"
+ S1->>S3: "我要当 Leader, 请投我一票"
+ S2->>S1: "同意, 投给你了"
+ S3->>S1: "同意, 投给你了"
+ Note over S1: "获得多数票, 我是 Leader 了"
+ S1->>S1: "开始执行故障转移..."
```
-### 故障转移步骤
+> [!TIP] 投票规则细节
+> - **先到先得**:每个 Sentinel 在一轮投票中只能投一票,投给第一个来拉票的
+> - **一轮只选一个**:如果多个 Sentinel 同时发起选举,票数不够就都不当选,等随机超时后重新发起(类似以太网的 CSMA/CD 避免冲突)
+> - **任期制**:每次故障转移都有一个 `configEpoch`(类似 Raft 的 term),防止旧 Leader 误操作
-当 Master 被判定 ODOWN 后,Sentinel Leader 执行以下流程:
+### 故障转移步骤详解
+
+Sentinel Leader 当选后,按以下顺序执行故障转移。整个过程通常在 **几秒到几十秒** 内完成:
```mermaid
flowchart TD
- A["1. 选出最佳 Replica"] --> B["评估标准:
复制偏移量 > 优先级 > RunID"]
- B --> C["SLAVEOF NO ONE
提升为新 Master"]
- C --> D["其他 Replica → SLAVEOF new-master"]
- D --> E["更新集群元数据"]
- E --> F["通知客户端新地址"]
+ A["Step 1: 选出最佳 Replica"] --> B["Step 2: 提升为新 Master"]
+ B --> C["Step 3: 让其他 Replica 转向新 Master"]
+ C --> D["Step 4: 通知客户端"]
+ D --> E["Step 5: 旧 Master 回来后自动降级"]
```
-**最佳 Replica 选择标准**(按优先级排序):
-1. **复制偏移量最大** — 离最新数据的 Replica 优先
-2. **`replica-priority` 最小** — 值越小越优先当选(0 = 不可当选)
-3. **Run ID 字典序最小** — 作为最终 tie-breaker
+**Step 1:选出最佳 Replica** — "谁来接班?"
+
+从所有健康的 Replica 中选出数据最新的一个,选择标准按优先级排序:
+
+| 优先级 | 评估维度 | 含义 | 举例 |
+|--------|---------|------|------|
+| 1(最高) | **复制偏移量最大** | 同步进度最接近 Master | Replica-A offset=10000, Replica-B offset=9998 → 选 A |
+| 2 | **`replica-priority` 最小** | 配置中人为指定的优先级 | priority=10 优先于 priority=100;priority=0 表示"永不选我" |
+| 3(最低) | **Run ID 字典序最小** | 纯粹的 tie-breaker | 两个 Replica 前两项完全相同,选 Run ID 字母序靠前的 |
+
+**Step 2:提升为新 Master**
+
+Sentinel 向选中的 Replica 发送 `SLAVEOF NO ONE`,使其断开与旧 Master 的复制关系,变成独立的 Master。
+
+**Step 3:让其他 Replica 转向新 Master**
+
+Sentinel 向其余 Replica 发送 `SLAVEOF `,它们开始从新 Master 同步数据。
+
+> [!NOTE] `parallel-syncs` 的作用
+> Sentinel 配置中的 `parallel-syncs` 控制**同时转向新 Master 的 Replica 数量**。设为 1 表示一个接一个地切换,避免所有 Replica 同时中断服务去同步(同步期间可能短暂不可用)。
+
+**Step 4:通知客户端**
+
+Sentinel 通过 Pub/Sub 机制在 `+switch-master` 频道广播新 Master 的地址。支持 Sentinel 协议的客户端库(如 go-redis)会自动订阅并切换。
+
+**Step 5:旧 Master 回来后怎么办?**
+
+旧 Master 恢复上线后,Sentinel 会自动把它配置为新 Master 的 Replica——它从"领导"变成了"下属",不会再抢回 Master 身份。
### Sentinel 配置文件
```conf
+# ── 核心配置:监控哪个 Master ──
sentinel monitor mymaster 192.168.1.10 6379 2
-# ↑ name ↑ host port quorum(达成SDOWN共识的个数)
+# ↑ Master地址 ↑ 端口 ↑ Quorum(至少几个 Sentinel 同意才判定 ODOWN)
-sentinel auth-pass mymaster your-password
+# ── 认证 ──
+sentinel auth-pass mymaster your-password # Master 设置了密码时必填
-# 多久无响应即标记 SDOWN(毫秒)
-sentinel down-after-milliseconds mymaster 30000
+# ── 三个最重要的时间参数 ──
-# 故障转移超时(毫秒),超过则失败重试
-sentinel failover-timeout mymaster 180000
+# 1. 主观下线阈值:Master 多久没回 PING 就标记 SDOWN
+# 不要太低!网络抖动 1~2 秒在生产环境很常见
+sentinel down-after-milliseconds mymaster 30000 # 30 秒(推荐 15~30 秒)
-# 最多多少 Replica 同时与新 Master 同步
+# 2. 故障转移超时:一次 Failover 最多花多长时间
+# 超时则认为本次转移失败,等下次重试
+sentinel failover-timeout mymaster 180000 # 3 分钟
+
+# 3. 并行同步数:同时转向新 Master 的 Replica 个数
+# 设为 1 = 逐个切换,保证至少有部分 Replica 始终可用
sentinel parallel-syncs mymaster 1
```
> [!WARNING] Sentinel 配置陷阱
-> - `down-after-milliseconds` 不要设太低(如 1s),网络抖动会导致误判;建议 **15~30 秒**
-> - `failover-timeout` 不宜太短,防止频繁重试加重系统负载
-> - `parallel-syncs` 设为 1 可避免转移期间所有 Replica 同时不可用
-> - **生产环境建议手动维护 Sentinel 配置**,避免自动感知的不确定性
-> - Redis 6+ 启用 ACL 后,需配置 `sentinel user/myuser password/mypass` 而非 `auth-pass`
+> - **`down-after-milliseconds` 不要太低**(如 1s)——网络抖动会触发误判,导致不必要的故障转移。建议 15~30 秒
+> - **`failover-timeout` 不宜太短**——如果 Master 数据量大,Replica 同步需要时间,太短会导致 Failover 反复超时失败
+> - **`parallel-syncs` 设为 1**——虽然切换慢一点,但保证服务不停;设为 0 或很大则所有 Replica 同时中断服务去同步
+> - **生产环境建议手动维护 Sentinel 配置文件**——虽然 Sentinel 有自动感知功能,但配置文件中的信息更可控
+> - **Redis 6+ ACL 兼容**——启用 ACL 后,用 `sentinel user myuser >password` 替代 `auth-pass`
### Sentinel 下的客户端连接
-Go 和 Python 生态都提供了原生支持——客户端内部自动发现 Master/Replica,故障时重新连接。
+> [!QUESTION] 故障转移后,客户端怎么知道新 Master 的地址?
+> 传统方式是客户端硬编码 Redis 地址——Master 换了 IP,所有客户端都得改配置重启。Sentinel 方案下,客户端**直接连接 Sentinel**(而不是 Redis),由 Sentinel 告诉客户端"谁是当前的 Master"。
+>
+> 主流语言的 Redis 客户端库都内置了 Sentinel 支持,原理是:
+> 1. 启动时向 Sentinel 查询当前 Master 地址
+> 2. 连接 Master 进行读写
+> 3. 如果连接失败,重新向 Sentinel 查询新地址
+
+**Go(go-redis)示例**——客户端自动发现和切换:
```go
-// go-redis: 原生 Sentinel 模式
rdb, err := redis.NewFailoverClient(&redis.FailoverOptions{
- MasterName: "mymaster",
- SentinelAddrs: []string{"sento1:26379", "sento2:26379", "sento3:26379"},
+ MasterName: "mymaster", // Sentinel 中配置的 Master 名称
+ SentinelAddrs: []string{"sentinel1:26379", "sentinel2:26379", "sentinel3:26379"},
Password: "your-password",
- MaxRetries: 3,
+ MaxRetries: 3, // 连接失败重试次数
RetryDelay: 500 * time.Millisecond,
})
-// 读请求走 Replica,写请求走 Master
-_ = rdb.Get(ctx, "key")
+
+// 客户端自动感知 Master:读请求走 Replica(如果配置了读写分离),写请求走 Master
+_ = rdb.Set(ctx, "order:12345", data, 0) // 写 → Master
+val, _ := rdb.Get(ctx, "order:12345").Result() // 读 → 可能走 Replica
```
+**Python(redis-py)示例**:
+
```python
-# redis-py Sentinel
from redis.sentinel import Sentinel
-sentinel = Sentinel([('sento1', 26379), ('sento2', 26379), ('sento3', 26379)])
-master = sentinel.master_for('mymaster', password='your-password') # 写
-slave = sentinel.slave_for('mymaster', password='your-password') # 读
+# 连接 Sentinel 节点
+sentinel = Sentinel([
+ ('sentinel1', 26379),
+ ('sentinel2', 26379),
+ ('sentinel3', 26379),
+])
+
+# 获取 Master 和 Slave 连接(自动发现 + 故障切换)
+master = sentinel.master_for('mymaster', password='your-password') # 用于写
+slave = sentinel.slave_for('mymaster', password='your-password') # 用于读
+
+master.set('key', 'value') # 写入 Master
+value = slave.get('key') # 从 Replica 读取
```
-## 五、生产实践 checklist
+## 六、生产实践 checklist
-### 部署前
+### 部署前 Checklist
-- [ ] **Sentinel 节点数 ≥ 3**,奇数个,部署在不同可用区或机器上
-- [ ] **Master + Replica 数量建议 1:2 ~ 1:3**,过多会影响同步延迟
-- [ ] **`replica-priority` 设值**:不可作为 Master 的设为 `0`
-- [ ] **持久化策略一致**:Master 和 Replica 的 RDB/AOF 策略应相同
+- [ ] **Sentinel 节点数 ≥ 3,且部署在不同机器/可用区** — 避免单机故障导致 Sentinel 丧失多数派
+- [ ] **Master + Replica 数量建议 1:2 ~ 1:3** — 太多 Replica 会加重 Master 同步负担;太少则冗余不足
+- [ ] **配置 `replica-priority`** — 不应被选为 Master 的 Replica 设为 `0`(如配置较低的从节点)
+- [ ] **持久化策略保持一致** — Master 和 Replica 都应开启 RDB 或 AOF,否则故障转移后新 Master 无持久化可能导致数据丢失
### 监控项
+日常巡检重点关注以下指标(建议每秒采样,配合 Prometheus/Grafana 看趋势):
+
```bash
-# 关键指标(每秒采样)
-INFO replication # connected_slaves, master_repl_offset
-INFO memory # used_memory_human (同步期间会突增)
-INFO stats # instantaneous_ops_per_sec
+# 复制状态 —— 最核心,异常时第一时间查这个
+redis-cli INFO replication
+# 关键字段:
+# connected_slaves:3 ← 在线 Replica 数量,突然减少说明有 Replica 掉线
+# master_repl_offset:12345678 ← Master 当前写入偏移量,持续增长说明正常
+# repl_backlog_active:1 ← Backlog 是否活跃
+# slave0:offset=12345600,lag=0 ← 每个 Replica 的同步偏移量和延迟
+
+# 内存 —— BGSAVE 期间内存会临时翻倍(fork 子进程)
+redis-cli INFO memory
+# 关键字段:
+# used_memory_human:2.50GB ← 同步期间如果骤增,可能触发 OOM
+
+# 每秒操作数 —— 发现流量突增或突降
+redis-cli INFO stats
+# 关键字段:
+# instantaneous_ops_per_sec:15000
```
### 故障排查流程
@@ -379,29 +561,42 @@ flowchart TD
### 常见坑点汇总
-| 坑 | 现象 | 解决 |
-|---|------|------|
-| Docker 环境下 IP 错乱 | Sentinel 记录了容器的内部 IP | 设置 `replica-announce-ip` |
-| 脑裂(Split Brain) | 网络分区后 Master 仍处理写请求 | 用 `min-replicas-to-write 1` 保护 |
-| OOM during BGSAVE | fork 子进程导致内存翻倍 | 关闭 THP,增大 swap,限制 maxmemory |
+| 坑 | 现象 | 根因 | 解决方案 |
+|---|------|------|------|
+| **Docker 环境 IP 错乱** | Sentinel 把容器内部 IP(如 172.17.0.x)当成 Master 地址告诉客户端,客户端连不上 | Docker 网络与宿主机网络不同 | 设置 `replica-announce-ip` 为宿主机 IP |
+| **脑裂(Split Brain)** | 网络分区后出现两个 Master 同时接受写入,分区恢复后一个 Master 的数据被覆盖丢失 | Master 与 Sentinel 网络断开,但与部分客户端仍连通 | 配置 `min-replicas-to-write` 保护 |
+| **BGSAVE 时 OOM** | 同步期间 Redis 内存突然翻倍,被 OOM Killer 杀掉 | fork 子进程使用 COW 机制,内存紧张时几乎复制整个数据集 | 关闭 THP、增大 swap、预留 50% 内存空间 |
-> [!NOTE] 防脑裂配置
+> [!NOTE] 脑裂是什么?怎么防?
+> **脑裂**指的是:Master 实际还活着(只是和 Sentinel 之间网络断了),但 Sentinel 以为它挂了,选出了一个新 Master。这时候**两个 Master 同时接受写入**。网络恢复后,旧 Master 变成新 Master 的 Replica,它的数据会被覆盖——写入旧 Master 的数据就丢了。
+>
+> **防御措施**:在 Master 端配置"至少要有 N 个 Replica 在线才接受写入":
> ```conf
-> # Master 端配置:至少 N 个 Replica 在线才接受写入
+> # 至少 1 个 Replica 在线且延迟不超过 10 秒,Master 才接受写入
> min-replicas-to-write 1
-> # 对应的最大允许延迟(秒),超时视为掉线
> min-replicas-max-lag 10
> ```
+> 这样当 Master 被隔离时(没有 Replica 在线),它会**拒绝写入**,而不是默默接受数据最终丢失的写操作。
-## 六、局限性与替代方案
+## 七、局限性与替代方案
-| 问题 | Sentinel 无法解决 |
-|------|----------------|
-| 单 Master 写入瓶颈 | ❌ 只有一个 Master |
-| 数据量超限单机容量 | ❌ |
-| 跨机房容灾 | ❌(Sentinel 跨网段延迟太高) |
+Sentinel 方案虽然解决了"高可用"问题,但它有明确的边界。理解这些边界才能选对方案:
-> [!TIP] 需要水平扩展?看 Cluster 方案 → [[hhs/Redis/07-集群方案]]
+| 场景 | Sentinel 能解决吗? | 为什么? | 应该用什么? |
+|------|----------------|---------|-----------|
+| Master 宕机自动恢复 | ✅ | 这正是 Sentinel 的核心功能 | — |
+| 读请求太多,单节点扛不住 | ✅ | 增加 Replica 分摊读压力 | 主从复制 |
+| **写请求太多,单 Master 瓶颈** | ❌ | 只有一个 Master 处理所有写入 | [[hhs/Redis/07-集群方案]] Cluster |
+| **数据量超过单机内存** | ❌ | 每个节点都存全量数据 | Cluster(数据分片) |
+| **跨机房容灾** | ❌ | 跨网段延迟太高,Sentinel 心跳会误判 | 异地多活架构 |
+
+> [!QUESTION] 什么时候该从 Sentinel 升级到 Cluster?
+> 当你遇到以下任一情况时,就该考虑 Cluster 了:
+> - 单 Master 写 QPS 持续超过 5w,垂直扩展(换更强的机器)已到极限
+> - 数据量超过单机内存(如 100GB+),且无法通过压缩优化
+> - 需要水平扩展写入能力(多个 Master 分片处理不同 key 的写入)
+>
+> → 详见 [[hhs/Redis/07-集群方案]]
## 关联笔记