481 lines
21 KiB
Markdown
481 lines
21 KiB
Markdown
---
|
||
tags: [Redis, 缓存, 高可用, Cluster, 分布式]
|
||
create time: 2026-05-15 18:14
|
||
---
|
||
|
||
# Redis Cluster 集群方案
|
||
|
||
## 概述
|
||
|
||
Redis Cluster 是 Redis 官方的**分布式**解决方案,通过数据分片(sharding)将数据分布在多个节点上,支持横向扩展和自动故障转移。核心设计:**哈希槽(Hash Slots)**而非一致性哈希。
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
C1["Client"] --> N1["Node A: slot 0~5460"]
|
||
C1 --> N2["Node B: slot 5461~10922"]
|
||
C1 --> N3["Node C: slot 10923~16383"]
|
||
|
||
N1 -->|"复制"| NA["Node A Replica"]
|
||
N2 -->|"复制"| NB["Node B Replica"]
|
||
N3 -->|"复制"| NC["Node C Replica"]
|
||
```
|
||
|
||
## 哈希槽机制
|
||
|
||
### 为什么不用一致性哈希?
|
||
|
||
> [!NOTE] 一致性哈希是什么?
|
||
> 一致性哈希是一种分布式分片算法,它将所有节点和数据映射到一个虚拟的**哈希环**上。数据沿顺时针找到的第一个节点就是它的归属。核心优势是:增减节点时,只需要迁移相邻区间的少量数据。更多细节见 [[hhs/Redis/07-集群方案/一致性哈希|一致性哈希]]。
|
||
|
||
| 对比项 | 一致性哈希 | 哈希槽 |
|
||
|--------|-----------|---------|
|
||
| 增节点迁移量 | **~N/K**(N 为总 key 数,K 为节点数) | **total_slots × 1/N** ≈ 每个新节点分得 ~2730 slots |
|
||
| 客户端透明性 | ✅ 通常透明,按哈希环顺时针归属 | ⚠️ 需要 MOVED/ASK 重定向 |
|
||
| 数据倾斜 | 依赖虚拟节点数量平衡 | ❌ slots 分配不均时会倾斜 |
|
||
| 复杂度 | 简单,纯哈希环 | 略复杂(Gossip + 槽位元数据同步) |
|
||
|
||
> [!QUESTION] 思考
|
||
> Redis 选择了 16384 个槽而不是其他数字,你觉得为什么是这个数?太少了会怎样?太多了又会怎样?
|
||
|
||
> [!NOTE] 为什么是 16384(2¹⁴)?
|
||
> Redis 作者 antirez 在 [GitHub Issue #2576](https://github.com/redis/redis/issues/2576) 中明确解释过:
|
||
> 1. **Gossip 消息大小限制**:每个节点的槽位信息用 bitmap 表示,16384 slots = 2KB(16384/8 字节),放入心跳包后带宽可控;若用 65536 slots 则需 8KB,心跳报文膨胀明显。
|
||
> 2. **节点数量上限**:Redis Cluster 官方建议最多 1000 个节点。16384 个槽在 1000 节点时,平均每个节点 16 个槽,粒度已经足够细。
|
||
> 3. **计算效率**:16384 是 2 的幂,CRC16 取模可直接用位运算 `& 0x3FFF`,无需真正执行除法。
|
||
>
|
||
> 因此,16384 是**槽位粒度**与**心跳带宽**之间的工程折中,而非哈希冲突的考量。
|
||
|
||
### 槽位计算
|
||
|
||
Redis 使用 **CRC16 算法**(Cyclic Redundancy Check,循环冗余校验——一种广泛用于数据校验的快速哈希算法)对 key 取模,结果映射到 0~16383 共 16384 个槽:
|
||
|
||
```bash
|
||
# CRC16(key) % 16384 = slot 编号
|
||
# 例:CRC16("foo") % 16384 = 5798 → 属于 Node B (5461~10922)
|
||
```
|
||
|
||
```go
|
||
// Go redis 库内部自动计算,开发者无需关心
|
||
rdb.Set(ctx, "foo", "bar", 0) // 自动路由到对应节点
|
||
```
|
||
|
||
> [!TIP] 多 key 在同一节点的技巧——用 `{}` 包裹 tag:
|
||
> ```
|
||
> SET {user:100}:name alice → slot X
|
||
> SET {user:100}:age 25 → 同 slot X
|
||
> SADD {user:100}:friends user:200
|
||
> ```
|
||
> 这样 `user:100` 的所有操作都在同一个节点上,支持 MULTI/LUA 事务。
|
||
|
||
> [!NOTE] hash tag 的边界
|
||
> - `{user:100}:name` 中只有 `{user:100}` 部分参与 hash 计算,`{}` 后面的 `:name` 是实际 key 的一部分。
|
||
> - 如果 key 中没有 `{}`,则整个 key 参与 hash 计算。
|
||
> - 务必确保 tag 逻辑正确,否则同一用户的不同属性可能分散在不同节点,导致事务失败。
|
||
|
||
## 通信协议与 Gossip 协议
|
||
|
||
> Gossip 协议是一种去中心化的信息传播协议,灵感来自"八卦传播"——每个节点定期向随机选择的邻居交换状态信息,几轮之后信息便传遍集群。Redis Cluster 用它同步节点状态和槽位分配,无需中心化协调者。详见 [[hhs/Redis/07-集群方案/Gossip协议|Gossip 协议]]。
|
||
|
||
### 节点间通信端口
|
||
|
||
```
|
||
节点通信端口 = client_port + 10000
|
||
例如: 6379 → 16379 (cluster bus)
|
||
```
|
||
|
||
> [!WARNING] 防火墙配置
|
||
> 生产环境中必须开放 `port` 和 `port+10000` 两个端口。很多云服务器安全组默认只开一个端口,会导致集群组建失败或节点状态异常。
|
||
|
||
### Gossip 协议运作方式
|
||
|
||
每个节点每秒向随机选择的节点发送 PING/PONG,维持集群状态的一致性。选择数量为固定公式:1 个长期未通信节点 + min(5, node_count/10) 个随机节点:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant A as Node A
|
||
participant B as Node B
|
||
|
||
loop 每秒钟
|
||
A->>B: PING (包含自身状态 + 随机选 3 个其他节点)
|
||
B->>A: PONG (含自身 + 选中节点的状态)
|
||
end
|
||
|
||
Note over A: Gossip 收敛原理:<br/>集群越大 fanout 越大,约 O(sqrt(N)),<br/>O(log(N)) 轮即可全同步
|
||
```
|
||
|
||
**Gossip 传播的三类信息:**
|
||
1. **PING/PONG** — 心跳,携带发送者的状态摘要
|
||
2. **MEET** — 手动加入集群,向指定节点发送 MEET 指令
|
||
3. **FAIL** — 某节点不可达,广播给其他节点
|
||
|
||
> [!QUESTION] 思考
|
||
> Redis 的 Gossip 选择策略(源码 `cluster.c:clusterCron()`):每次选 1 个长时间未通信的随机节点 + 5 个随机节点(上限为集群节点总数的 1/10),而非广播给所有节点。这是固定公式,不因集群规模动态切换。为什么不广播所有节点?1000 节点的集群中,每秒广播意味着 O(N²) 条消息——这正是 Gossip 用随机子集换取带宽的关键设计。
|
||
|
||
### PING/PONG 报文详解
|
||
|
||
```
|
||
PING 报文体包含:
|
||
├── 发送节点的 nodeId
|
||
├── 槽位分配表(哪些节点负责哪些 slot)
|
||
├── 节点状态(master/slave/fail)
|
||
├── 最后通信时间戳
|
||
└── 当前集群 epoch(配置版本号,每次集群拓扑变更如 failover 或 reshard 时递增,用于解决节点间的状态冲突——epoch 更大的配置被视为最新真相)
|
||
```
|
||
|
||
## 数据迁移流程
|
||
|
||
### 移动一个 slot(跨节点)
|
||
|
||
```bash
|
||
# 在源节点执行(准备阶段)
|
||
CLUSTER SETSLOT <slot_id> MIGRATING <target_node_id>
|
||
|
||
# 在目标节点执行(确认阶段)
|
||
CLUSTER SETSLOT <slot_id> IMPORTING <source_node_id>
|
||
|
||
# 逐 key 迁移
|
||
# GETKEYSINSLOT: 返回指定 slot 中的 key 列表,每次最多 100 个
|
||
# MIGRATE: 原子地将 key 从源节点转移到目标节点(同步阻塞,最后一个参数 5000 是超时 ms)
|
||
for key in $(redis-cli -h $SRC -p $SRC_PORT CLUSTER GETKEYSINSLOT $slot 100); do
|
||
redis-cli -h $SRC -p $SRC_PORT MIGRATE $DST $DST_PORT $key 0 5000
|
||
done
|
||
|
||
# 完成迁移
|
||
CLUSTER SETSLOT <slot_id> NODE <target_node_id>
|
||
```
|
||
|
||
> [!NOTE] 迁移期间的 ASK 临时定向
|
||
> 当 slot 处于 MIGRATING/IMPORTING 中间状态时:
|
||
> - 源节点**拥有该 key** → 正常处理
|
||
> - 源节点**找不到该 key**(已迁移走) → 返回 `ASK <target-node>`,客户端只需本次请求转发到目标节点,后续请求仍发原节点。
|
||
>
|
||
> 这保证了迁移过程对客户端透明,且不会造成路由表的提前混乱。
|
||
|
||
### 自动化迁移 —— redis-cli cluster reshard
|
||
|
||
```bash
|
||
redis-cli --cluster reshard node-host:6379 \
|
||
--cluster-from <src-node-id> \
|
||
--cluster-to <dst-node-id> \
|
||
--cluster-slots 5461 \
|
||
--cluster-yes
|
||
```
|
||
|
||
> [!WARNING] reshard 期间服务不中断
|
||
> 但涉及迁移的 slot 在高负载下可能有额外延迟,低峰期操作更稳妥。
|
||
|
||
## 创建集群
|
||
|
||
### 手动创建(3 Master + 3 Replica)
|
||
|
||
```bash
|
||
# 1. 启动 6 个实例(不同端口)
|
||
for port in $(seq 7001 7006); do
|
||
mkdir -p /tmp/$port && redis-server \
|
||
--port $port --cluster-enabled yes \
|
||
--appendonly yes --cluster-config-file nodes.conf \
|
||
--cluster-node-timeout 5000 \
|
||
--loglevel warning > /tmp/$port/redis.log 2>&1 &
|
||
done
|
||
|
||
# 2. 一行命令组建集群
|
||
redis-cli --cluster create \
|
||
127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 \
|
||
127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 \
|
||
--cluster-replicas 1
|
||
```
|
||
|
||
### Docker Compose(开发环境)
|
||
|
||
```yaml
|
||
version: "3.9"
|
||
services:
|
||
redis-1:
|
||
image: redis:7-alpine
|
||
command: redis-server --cluster-enabled yes --appendonly yes
|
||
ports: ["7001:7001"]
|
||
volumes: ["redis-data-1:/data"]
|
||
# ... 重复 redis-2 ~ redis-6
|
||
volumes:
|
||
redis-data-1:
|
||
# ...
|
||
```
|
||
|
||
### 连接集群客户端
|
||
|
||
```go
|
||
import "github.com/redis/go-redis/v9"
|
||
|
||
rdb := redis.NewClusterClient(&redis.ClusterOptions{
|
||
Addrs: []string{":7001", ":7002", ":7003", ":7004", ":7005", ":7006"},
|
||
Password: "your-password",
|
||
MaxRetries: 3,
|
||
})
|
||
defer rdb.Close()
|
||
|
||
// SET/GET 自动路由到正确的节点
|
||
rdb.Set(ctx, "key", "value", 0)
|
||
val, _ := rdb.Get(ctx, "key").Result()
|
||
```
|
||
|
||
## Cluster 的局限性
|
||
|
||
| 限制 | 说明 | 替代方案 |
|
||
|------|------|---------|
|
||
| 不支持多数据库 | 固定只有 db 0 | 应用层模拟 db(key 前缀) |
|
||
| 命令子集受限 | `KEYS`, `MIGRATE`, `SORT` 等不可用 | 用 `SCAN` 替代 `KEYS` |
|
||
| 跨节点多 key 命令受限 | `DEL`, `MGET`, `MSET`, `RENAME` 等多 key 操作要求 key 同 slot | 用 hash tag 或应用层拆分为多次单 key 调用 |
|
||
| Lua 脚本有限 | 所有 key **必须映射到同一 slot**;不支持非确定性函数(time/random) | 同 Lua 内 key 用同一 hash tag;用 `EVALSHA` 减少网络传输 |
|
||
| Pipeline 受限 | 同一 pipeline 中所有 key 必须在同一节点 | 用 hash tag `{user:100}:x` 对齐 |
|
||
| MULTI/EXEC 受限 | 同上,跨 key 事务不支持 | 用 Lua 脚本实现原子操作 |
|
||
|
||
> [!TIP] 解决 Pipeline 跨节点问题
|
||
> ```go
|
||
> // ❌ 错误:key 分布在不同节点
|
||
> pipe.Pipeline().
|
||
> Get(ctx, "user:1:name").
|
||
> Get(ctx, "user:2:name")
|
||
>
|
||
> // ✅ 正确:用 hash tag 对齐到同一节点
|
||
> pipe.Pipeline().
|
||
> Get(ctx, "{user:1}:name").
|
||
> Get(ctx, "{user:1}:age")
|
||
> ```
|
||
|
||
## 运维命令速查
|
||
|
||
```bash
|
||
# 集群状态
|
||
redis-cli --cluster check 127.0.0.1:7001
|
||
|
||
# 查看节点信息
|
||
redis-cli -p 7001 CLUSTER NODES
|
||
# 输出格式: nodeId ip:port@busPort flags master/slot-range self
|
||
|
||
# 示例输出解析
|
||
e3a1b2c3... 192.168.1.10:7001@17001 master - 0 1715000000000 1 connected 0-5460
|
||
# ↑ node-id ↑ addr ↑ bus-port ↑ role ↑ epoch ↑ slots
|
||
|
||
# 统计各节点 key 数量
|
||
redis-cli -p 7001 DBSIZE
|
||
redis-cli -c -p 7001 --cluster call DBSIZE # 在所有节点执行
|
||
|
||
# 强制某个 slot 下线(紧急情况)
|
||
redis-cli --cluster fix 127.0.0.1:7001
|
||
```
|
||
|
||
## 扩容指南
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
Before["3 Master<br/>各 5461 slots"] -->|添加 3 新节点| Middle["6 Master<br/>slot 全空"]
|
||
Middle -->|reshard 迁 slot| After["6 Master<br/>各 ~2730 slots"]
|
||
After -->|加 replica| Final["6 Master + 6 Replica"]
|
||
```
|
||
|
||
> [!TIP] 扩容节奏建议
|
||
> 1. 先加 Master 并分 slot
|
||
> 2. 再给每个 Master 配 Replica
|
||
> 3. 分批迁移(每次 1~2 台),避免流量剧变
|
||
|
||
## 故障转移机制(Failover)
|
||
|
||
> [!QUESTION] 思考
|
||
> 假设一个 Master 宕机了,谁来决定哪个 Replica 接任?如果让客户端来判断,会有什么问题?
|
||
|
||
### 故障检测三阶段
|
||
|
||
> [!NOTE] Raft 式投票是什么?
|
||
> Raft 是一种分布式共识算法,核心思想之一是**领导者选举**:当 Leader 宕机时,Follower 发起选举,获得多数派投票的节点成为新 Leader。Redis Cluster 的 Failover 选举借鉴了这个机制——Replica 通过递增 epoch 并向所有 Master 请求投票,获得多数票后晋升为新 Master。但 Redis 只使用了 Raft 的选举部分,数据复制仍然走自身协议。详见 [[hhs/Redis/07-集群方案/Raft共识算法|Raft 共识算法]]。
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A["PFAIL 潜在失效<br/>单个节点判定"] -->|"超过 cluster-node-timeout"| B["FAIL 确认失效<br/>多数 Master 同意"]
|
||
B -->|"Raft 式投票"| C["选举 Leader<br/>Replica 竞选"]
|
||
C -->|"最高优先级获胜"| D["Failover<br/>接管 slots"]
|
||
|
||
style A fill:#fff3e0,stroke:#ff9800
|
||
style B fill:#fce4ec,stroke:#e91e63
|
||
style C fill:#e8eaf6,stroke:#3f51b5
|
||
style D fill:#e8f5e9,stroke:#4caf50
|
||
```
|
||
|
||
**详细流程:**
|
||
|
||
| 阶段 | 谁触发 | 条件 | 持续时间 |
|
||
|------|--------|------|---------|
|
||
| **PFAIL** | 单个节点 | 超过 `cluster-node-timeout` 未收到 PONG | 瞬时 |
|
||
| **FAIL** | PFAIL 节点广播 | 大多数 Master 同意该节点不可达 | 秒级 |
|
||
| **选举** | 宕机 Master 下唯一存活 Replica | 发起集群纪元递增 + Raft 投票 | 秒级 |
|
||
| **Failover** | 当选 Leader | 提升为 Master,接管原 Master 的 slots | 亚秒级 |
|
||
|
||
### 选举规则
|
||
|
||
1. **只有 Replica 能参与选举**,Master 不会主动抢占
|
||
2. **选举时机**:Replica 发现自己复制的 Master 进入 FAIL 状态
|
||
3. **竞争排序依据**(依次比较):
|
||
- `replica-priority` 值越小优先级越高(配置为 0 表示永远不参选)
|
||
- 复制偏移量(`replication offset`)越大越优先——偏移量大意味着数据最新
|
||
- 若偏移量相同,`node ID` 字典序小的优先(确定性 tiebreaker,避免同时发起投票)
|
||
4. **Raft 式投票**:每个 Master 一票,先到先得
|
||
5. **获胜条件**:获得多数 Master 的选票
|
||
6. **升级步骤**:
|
||
- 将自己提升为 Master
|
||
- 接管所有原 Master 管理的 slots
|
||
- 向所有节点广播新的配置(configEpoch 递增)
|
||
|
||
> [!NOTE] 脑裂保护
|
||
> 脑裂是指网络分区把集群割裂成多个子集群,各自选举出 Master 同时对外服务。Redis 通过 **epoch 版本号** 解决冲突——epoch 更小的 Master 被强制降级。此外,建议配置 `min-replicas-to-write 1`,当 Master 联系不到足够 Replica 时主动拒绝写入,防止分区恢复后数据丢失。详见 [[hhs/Redis/07-集群方案/脑裂问题|脑裂问题]]。
|
||
|
||
### 客户端容错处理
|
||
|
||
集群模式下客户端会收到两种重定向响应:
|
||
|
||
```go
|
||
// MOVED: 永久重定向(槽位已迁移到新节点)
|
||
// 客户端应更新拓扑,后续请求直接发到目标节点
|
||
MOVED 3999 192.168.1.20:7002
|
||
|
||
// ASK: 临时重定向(正在迁移中)
|
||
// 单次请求发送到目标节点,后续仍发原节点
|
||
ASK 3999 192.168.1.20:7002
|
||
```
|
||
|
||
> [!TIP] 重试策略要点
|
||
> - MOVED 后更新本地路由表,**不再重试旧节点**
|
||
> - ASK 只重试**这一次**,之后恢复原来的路由
|
||
> - 如果连续收到 MOVED/ASK,说明集群正在剧烈变更,适当退避
|
||
|
||
## 软客户端 vs 硬客户端
|
||
|
||
> [!QUESTION] 思考
|
||
> 客户端拿到第一个 MOVED 响应后才去查询新拓扑,还是从一开始就维护完整的集群地图?哪种方式在扩容时更稳定?
|
||
|
||
### 两种模式对比
|
||
|
||
| 特性 | 软客户端(Lazy Mode) | 硬客户端(Eager Mode) |
|
||
|------|---------------------|----------------------|
|
||
| 初始化行为 | 连接任一节点获取初始拓扑 | 预取所有节点完整拓扑 |
|
||
| 拓扑更新 | 遇到 MOVED/ASK 时按需刷新 | 后台定时拉取,提前感知变化 |
|
||
| 扩容时体验 | 迁移过渡期频繁 MOVED,请求偶发延迟 | 平滑无感,提前知道新 slot 在哪 |
|
||
| 内存开销 | 仅存储已知节点 | 存储全部节点 + slot→node 映射表 |
|
||
| 代表库 | lettuce(默认 lazy) | go-redis, jedis(内置拓扑缓存) |
|
||
|
||
```go
|
||
// go-redis 示例:硬客户端,自动维护拓扑
|
||
rdb := redis.NewClusterClient(&redis.ClusterOptions{
|
||
Addrs: []string{":7001", ":7002", ":7003"},
|
||
PoolSize: 20, // 每个节点独立连接池
|
||
MinIdleConns: 5, // 保持最小空闲连接
|
||
ReadTimeout: 3 * time.Second, // 读超时
|
||
WriteTimeout: 3 * time.Second, // 写超时
|
||
IdleTimeout: 5 * time.Minute, // 连接空闲回收
|
||
})
|
||
|
||
// 连接池隔离:每个节点有独立的连接池,互不影响
|
||
// GET 请求 go-redis 会自动判断 key 所在的 slot 并路由到正确节点
|
||
val, err := rdb.Get(ctx, "user:100:name").Result()
|
||
```
|
||
|
||
```java
|
||
// lettuce 示例:软客户端,按需刷新拓扑(Java/Spring)
|
||
RedisClusterClient client = RedisClusterClient.create("redis://node1:7001");
|
||
StatefulRedisClusterConnection<String, String> conn = client.connect();
|
||
// 遇到 MOVED 时,ClusterTopologyRefreshScheduler 自动重新发现拓扑
|
||
String val = conn.sync().get("key");
|
||
```
|
||
|
||
> [!TIP] 推荐配置
|
||
> 生产环境建议采用硬客户端 + 合理连接池大小。每个节点独立连接池可以避免热点 key 占满单个节点的连接资源。
|
||
|
||
## 生产部署关键注意点
|
||
|
||
### 网络与安全
|
||
|
||
| 项目 | 建议值/做法 | 原因 |
|
||
|------|------------|------|
|
||
| 防火墙 | 开放 `port` 和 `port+10000` | 集群总线通信依赖双端口 |
|
||
| TLS | Redis 7.0+ 支持 TLS(需编译开启) | 加密集群总线通信,防止拓扑泄露 |
|
||
| ACL | Redis 6+ 启用 ACL 认证 | 替代传统 AUTH,更细粒度权限控制 |
|
||
| bind | 绑定内网 IP,不暴露公网 | 集群总线不鉴权,外网可控制整个集群 |
|
||
|
||
### 参数调优
|
||
|
||
| 参数 | 推荐值 | 说明 |
|
||
|------|--------|------|
|
||
| `cluster-node-timeout` | 15000 ms | 不要太短,避免网络抖动误判;不要长于业务容忍度 |
|
||
| `maxmemory-policy` | 缓存场景 `allkeys-lru`;持久化场景 `noeviction` | 缓存类应用优先 LRU 避免 OOM;需要严格保证写入成功的场景用 `noeviction`,由业务层处理满错误 |
|
||
| `appendonly` | `yes` | 集群推荐 AOF + 每秒刷盘,兼顾性能与安全 |
|
||
| `cluster-replica-validity-factor` | 10 | 乘以 timeout 作为复制链路容忍阈值,0 表示不拒绝主从链接 |
|
||
| `cluster-require-full-coverage` | `yes`(默认) | `yes` 时任意 slot 无主节点则集群整体拒绝写入;生产环境若追求可用性可设 `no`,允许部分 slot 不可用时其余 slot 正常服务 |
|
||
| `cluster-allow-reads-when-down` | `no` | 设为 `yes` 时集群即使处于 FAIL 状态也允许读请求(前提:该 key 所在 slot 有可达节点),适合读密集型业务容忍短暂降级 |
|
||
|
||
> [!IMPORTANT] `cluster-require-full-coverage` 的取舍
|
||
> 当某个 Master 和它的所有 Replica 同时宕机时,该 Master 负责的 slot 就"裸奔"了。
|
||
> - **`yes`(默认)**:集群整体拒绝读写,宁可停机也不允许数据不一致——适合对一致性要求极高的金融场景。
|
||
> - **`no`**:集群只拒绝访问故障 slot 的请求,其他 slot 继续服务——适合追求可用性的互联网业务,牺牲部分可用 slot 换取整体不宕机。
|
||
|
||
### 架构最佳实践
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
App["你的应用服务"] --> SA["Service A<br/>:7001-7007"]
|
||
App --> SB["Service B<br/>:7004-7007"]
|
||
App --> SC["Service C<br/>:7010-7017"]
|
||
App --> SD["Service D<br/>:7016-7023"]
|
||
SA --> C1["Cluster 1<br/>独立集群"]
|
||
SB --> C2["Cluster 2<br/>独立集群"]
|
||
SC --> C3["Cluster 3<br/>独立集群"]
|
||
SD --> C4["Cluster 4<br/>独立集群"]
|
||
|
||
style App fill:#e3f2fd,stroke:#1565c0
|
||
style C1,C2,C3,C4 fill:#e8f5e9,stroke:#4caf50
|
||
```
|
||
|
||
> [!IMPORTANT] 原则
|
||
> 1. **一服一群**:不同业务线使用独立集群,避免互相干扰
|
||
> 2. **奇数 Master**:选举需要多数派,偶数 Master 不如少一个 Master 多一个 Slave
|
||
> 3. **至少 3 Master + 3 Replica**:单 Master 没有高可用意义
|
||
> 4. **数据隔离**:不要用 `FLUSHALL`,集群里会影响所有节点
|
||
> 5. **大 key 慎用**:集群下大 key 无法拆分,会导致单个节点内存/带宽瓶颈
|
||
|
||
## 大 Key 的检测与处理
|
||
|
||
> [!WARNING] 为什么集群下大 Key 更危险?
|
||
> 单机 Redis 的大 Key 只影响自己;集群模式下,大 Key 不可拆分——它**固定落在某个 slot、某台机器**。这台机器的 CPU、内存、带宽瞬间成为瓶颈,相当于把集群退化成了单点。
|
||
|
||
**检测:**
|
||
|
||
```bash
|
||
# redis-cli 自带扫描(Redis 4.0+)
|
||
redis-cli --bigkeys # 只统计每种类型中最大的 key
|
||
redis-cli --memkeys # 按内存占用排序,更精确
|
||
redis-cli --bigkeys --i 0.01 # 控制扫描间隔(秒),避免线上打满带宽
|
||
|
||
# 输出示例
|
||
# [00.00%] Biggest string found so far 'user:100:feed' with 536870912 bytes
|
||
# [00.01%] Biggest hash found so far 'order:cache' with 876543 entries
|
||
```
|
||
|
||
**处理策略:**
|
||
|
||
| 类型 | 问题 | 拆分方案 |
|
||
|------|------|---------|
|
||
| 大 String | `GET` 一次传输全量数据 | 拆成多个子 key:`item:1:chunk1`、`item:1:chunk2` |
|
||
| 大 Hash | `HGETALL` 阻塞,cursor 遍历慢 | 按字段分桶:`user:100:profile:v1`、`user:100:profile:v2` |
|
||
| 大 List | `LRANGE` 一次返回全量 | 只保留最近 N 条,历史数据归档到 DB/ES |
|
||
| 大 Set/ZSet | `SMEMBERS`/`ZREVRANGE` 带宽打满 | 分段查询 + 游标,或业务层拆成多个小集合 |
|
||
|
||
> [!TIP] 异步删除(Redis 4.0+)
|
||
> 对已有的大 Key,优先用 `UNLINK`(异步删除)替代 `DEL`(同步阻塞),防止删除操作拖垮主线程。删除前用 `DEBUG OBJECT key` 查看 `serializedlength` 估算影响范围。
|
||
|
||
## 关联笔记
|
||
|
||
- [[hhs/Redis/07-集群方案/一致性哈希]] — 一致性哈希原理与虚拟节点
|
||
- [[hhs/Redis/07-集群方案/Gossip协议]] — Gossip 协议的传播原理与收敛特性
|
||
- [[hhs/Redis/07-集群方案/Raft共识算法]] — Raft 领导者选举与 Redis Failover
|
||
- [[hhs/Redis/07-集群方案/脑裂问题]] — 脑裂问题的本质与防护策略
|
||
- [[hhs/Redis/06-主从与哨兵]] — Sentinel 单主高可用方案
|
||
- [[hhs/Redis/11-运维与性能调优]] — 监控指标与告警
|
||
- [[hhs/Redis/README]] — 知识索引总览
|