Redis 分片集群(Cluster)
Redis Cluster 是官方提供的分布式运行方案,通过哈希槽将数据分散存储在多个 Redis 实例上,实现海量数据存储和高并发写入能力。
1. 集群概述
1.1 什么是 Redis Cluster
Redis Cluster 从 Redis 3.0 开始正式提供(2015 年),是一个服务端分片(Server-side Sharding)方案。它将数据自动切分到多个 Redis 节点上,每个节点只负责整体数据的一部分。
Redis Cluster 核心解决两大问题:
| 问题 | 单机/主从的局限 | Cluster 如何解决 |
|---|---|---|
| 海量数据存储 | 单机内存上限(通常 < 64GB),无法水平扩展 | 数据按槽分散到多个 master,总容量 = N x 单节点容量 |
| 高并发写入 | 单机写 QPS 上限(通常 < 10w),所有写入走 master | 多个 master 各自承担部分写入,总写 QPS = N x 单节点写 QPS |
1.2 集群特征
Redis Cluster 拓扑(3主3从)
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Master A │ │ Master B │ │ Master C │
│ 槽 0-5460 │ │ 槽 5461-10922│ │槽 10923-16383│
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
│ Slave A │ │ Slave B │ │ Slave C │
│ (副本) │ │ (副本) │ │ (副本) │
└───────────┘ └───────────┘ └───────────┘
每个 master 保存不同的槽数据
每个 slave 是其 master 的副本(读负载 + 容灾)
master 之间通过 Gossip 协议 PING/PONG 相互监测核心特征总结:
| 特征 | 说明 |
|---|---|
| 多 master 共存 | 每个 master 负责一部分哈希槽,存储不同的数据子集 |
| 每个 master 可有多个 slave | slave 提供读服务和故障接管能力 |
| master 间 PING 监测 | 使用 Gossip 协议(P2P)进行健康检查和信息交换 |
| 自动故障转移 | master 宕机后,其 slave 通过选举自动提升为 new master |
| 自动请求转发 | 客户端连错节点时,通过 MOVED/ASK 重定向到正确节点 |
| 无中心节点 | 没有 proxy 或协调节点,客户端直接连接任意节点 |
1.3 架构对比:单机 vs 主从+哨兵 vs Cluster
| 维度 | 单机 | 主从 + Sentinel | Redis Cluster |
|---|---|---|---|
| 数据分片 | 无,全部数据在一个实例 | 无,全部数据在主节点 | 有,数据按哈希槽分散到多个 master |
| 容量上限 | 单机内存(如 64GB) | 单机内存 | N x 单节点内存(理论上无限) |
| 写入能力 | 单机写 QPS | 单 master 写入 | N 个 master 并行写入 |
| 读取能力 | 单机读 QPS | 多 slave 分担读 | N 组 master+slave 分担读写 |
| 高可用 | 无(单点故障) | 自动故障转移(秒级) | 自动故障转移(秒级) |
| 客户端复杂度 | 低 | 低(Sentinel 发现) | 中(需要 Smart Client 或代理) |
| 运维复杂度 | 低 | 中(Sentinel 集群 3-5 节点) | 较高(节点管理、槽迁移) |
| 多 key 操作 | 完全支持 | 完全支持 | 仅支持同槽 key |
| 事务/Lua | 完全支持 | 完全支持 | 仅支持同节点同槽 key |
| 适用场景 | 开发测试、小数据量 | 数据量 < 内存上限、读多写少 | 大数据量、高并发读写 |
1.4 Cluster 不支持的操作
🚨 以下操作在 Cluster 模式下会失败——在设计阶段就要注意避开:
| 不支持的操作 | 原因 | 错误示例 |
|---|---|---|
| 多 key 跨槽操作 | 多个 key 可能分布在不同节点,无法原子执行 | MSET key1 key2 key3(key1 和 key2 在不同槽) |
| 事务跨 key | MULTI/EXEC 中的所有 key 必须在同一槽(同一节点) | MULTI → SET {a} v1 → SET {b} v2 → EXEC |
| Lua 脚本跨 key | EVAL/EVALSHA 中操作的所有 key 必须属于同一槽 | EVAL "redis.call('SET',KEYS[1],...)" 2 key1 key2 |
| SELECT 多数据库 | Cluster 只支持 db 0 | SELECT 1 → (error) ERR SELECT is not allowed in cluster mode |
| WATCH 跨 key | WATCH 的 key 必须在同一节点 | WATCH key1 key2(不同槽会失败) |
💡 解决方案:通过 Hash Tag(第 7 节)将相关 key 强制分配到同一槽。
2. 哈希槽(Hash Slot)
2.1 为什么用哈希槽而不是一致性哈希
许多分布式系统(如 Memcached 集群)使用一致性哈希来分配数据。Redis Cluster 选择了不同的方案——哈希槽(Hash Slot)。
一致性哈希的问题
一致性哈希环
key3 ↙ ↖ key1
(Node C) (Node A)
/ \
/ [环形] \
| 空间 |
\ /
\ /
key4 ↗ Node B ↖ key2
问题 1:加减节点 → 整个环重新映射 → 大量数据迁移
问题 2:虚拟节点数选择 → 少了不均匀,多了内存开销大
问题 3:实现复杂 → 二分查找定位节点,代码复杂度高| 一致性哈希的缺陷 | 详细说明 |
|---|---|
| 加减节点迁移量大 | 新增节点时,相邻节点的数据需要重新计算归属,涉及大量 key 迁移 |
| 数据可能不均匀 | 即使引入虚拟节点,在节点较少时仍可能出现热点 |
| 实现复杂度高 | 需要维护哈希环、虚拟节点映射、二分查找定位等 |
哈希槽的优势
Redis Cluster 将整个键空间划分为 16384 个槽(slot),每个 master 负责一部分槽:
哈希槽空间(0 ~ 16383)
┌──────────────┬────────────────┬────────────────┐
│ 0 ~ 5460 │ 5461 ~ 10922 │ 10923 ~ 16383 │
│ Master A │ Master B │ Master C │
└──────────────┴────────────────┴────────────────┘
每个 key 通过 CRC16 取模定位到槽,再根据槽找到对应的 master| 哈希槽的优势 | 详细说明 |
|---|---|
| 加减节点只移动槽 | 新加节点时,只需从现有节点迁移部分槽过去,不重新哈希所有 key |
| 16384 个固定槽 | 槽数量固定,无论节点多少,key 到槽的映射永远不变 |
| 均匀分布 | CRC16(key) % 16384 取模,在 key 足够多时分布天然均匀 |
| 实现简单 | 槽→节点映射只是一张查找表(16384 个槽对应 16384 个节点指针) |
🔬 为什么是 16384
16384(2 的 14 次方)这个数字是精心选择的,不是随意设定的:
| 考虑因素 | 详解 |
|---|---|
| 心跳包大小 | 每个节点通过 Gossip 协议的 PING 消息携带自身负责的槽信息(bitmap 表示),16384 个槽 = 2048 字节(16384 bits / 8),在 TCP 包中刚好可控。如果是 65536 个槽,bitmap 需要 8KB,心跳开销大幅增加 |
| 网络传输效率 | 2048 字节的 bitmap 可以在一个 TCP 分段中传输,几乎不会分片。8KB 的 bitmap 则需要多个分段 |
| 节点数量适配 | Redis 官方建议集群不超过 1000 个节点。16384 / 1000 ≈ 16 个槽/节点,粒度足够精细 |
| 客户端内存 | 客户端需要缓存槽→节点映射表,16384 个条目占用的内存很小 |
| 配置传播 | CLUSTER NODES 等命令需要传输槽分配信息,16384 个槽以 bitmap 形式只需约 2KB |
槽数量选择的权衡:
太少(如 1024):
→ 大集群中每节点只分到少数几个槽
→ 粒度粗,槽迁移灵活性差
→ 但心跳包更小
16384(实际选值):
→ 足够精细,即使 1000 节点也每节点 ~16 槽
→ bitmap 仅 2KB,心跳成本低廉
→ 一个 TCP 包刚好装下
太多(如 65536):
→ 粒度超细,但没必要
→ bitmap 需 8KB → 心跳成本高 4 倍
→ 客户端缓存变大2.2 槽分配
每个 master 负责一部分槽(可以是不连续的范围)。集群初始化时,slot 由管理员分配或通过 redis-cli --cluster create 自动分配。
示例:3 个 master 的槽分配
# Master A: 槽 0-5460(共 5461 个槽)
# Master B: 槽 5461-10922(共 5462 个槽)
# Master C: 槽 10923-16383(共 5461 个槽)
# 总槽数:5461 + 5462 + 5461 = 16384每个节点在内存中维护一张槽→节点映射表:
// 伪代码:每个节点维护的集群状态
struct clusterState {
dict *nodes; // 所有已知节点
clusterNode *slots[16384]; // 槽→节点映射:对于每个槽,指向负责该槽的节点
// ...
};这个映射表是集群正常工作的核心——每个命令到达任一台节点后,节点通过查表判断 key 是否属于自己的槽,如果不是,返回 MOVED 重定向。
2.3 命令路由
客户端执行命令时的路由流程:
客户端执行 SET user:1001 "Alice" 的完整流程:
1. 计算槽号
slot = CRC16("user:1001") % 16384
→ 假设结果为 12250
2. 查本地缓存
槽 12250 → 节点 192.168.1.3:6379(Master C)
3. 如果当前连接就是 Master C
→ 直接执行,返回 OK
4. 如果当前连接是其他节点
→ 发送命令
→ 节点返回 MOVED 12250 192.168.1.3:6379
→ 客户端切换到 Master C
→ 重发命令🔬 MOVED 重定向(永久性转移)
当 key 不在当前节点时,节点返回 MOVED 错误:
客户端 错误节点 正确节点
│ │ │
│ SET user:1001 "Alice" │ │
│─────────────────────────────────────►│ │
│ │ │
│ -MOVED 12250 192.168.1.3:6379 │ │
│◄─────────────────────────────────────│ │
│ │ │
│ 更新本地缓存:槽 12250 → 192.168.1.3 │ │
│ │ │
│ SET user:1001 "Alice" │ │
│─────────────────────────────────────────────────────────►│
│ │ │
│ +OK │ │
│◄─────────────────────────────────────────────────────────│
│ │ │MOVED 错误的格式:
-MOVED <slot> <ip>:<port>- 表示槽已经永久转移到目标节点
- 客户端收到后应立即更新本地槽映射缓存
- 支持 MOVED 重定向的客户端称为 Smart Client(如 JedisCluster、Lettuce、go-redis)
💡 最佳实践:生产环境必须使用支持 MOVED 重定向的 Smart Client。
redis-cli -c也支持自动重定向,但仅用于运维调试。
🔬 ASK 重定向(临时性转移)
当槽正在迁移时,源节点返回 ASK 错误:
客户端 源节点(迁移中) 目标节点
│ │ │
│ GET user:1001 │ │
│─────────────────────────────────────►│ │
│ │ │
│ -ASK 12250 192.168.1.4:6379 │ │
│◄─────────────────────────────────────│ │
│ │ │
│ ASKING │ │
│──────────────────────────────────────────────────────────►│
│ GET user:1001 │ │
│──────────────────────────────────────────────────────────►│
│ │ │
│ $5 │ │
│ Alice │ │
│◄──────────────────────────────────────────────────────────│
│ │ │ASK 重定向的关键行为:
- 客户端收到 ASK 错误后,向目标节点发送
ASKING命令 - 然后再发原始命令
- ASKING 是一次性的——只允许接下来的那条命令绕过"该槽不属于我"的检查
- 客户端不应更新本地槽缓存(槽迁移还在进行中,尚未永久转移)
MOVED vs ASK 对比
| 维度 | MOVED | ASK |
|---|---|---|
| 含义 | 槽已永久转移到目标节点 | 槽正在迁移中,临时指向目标节点 |
| 客户端更新缓存 | 是——更新槽→节点映射 | 否——不要更新缓存 |
| 下次相同 key 请求 | 直接发到新节点 | 仍发到原节点 |
| 预处理命令 | 无 | 需先发 ASKING 命令 |
| 目标节点行为 | 正常处理 | 需要客户端先发 ASKING 才处理(否则拒绝——该槽还不属于自己) |
| 发生阶段 | 槽迁移完成后广播 | 槽迁移进行中 |
3. 🔬 Gossip 协议
3.1 协议概述
Redis Cluster 节点之间使用 Gossip 协议(流言协议)进行通信,这是一个去中心化的 P2P 协议。没有任何中心协调节点,每个节点平等地与其他节点交换状态信息。
Gossip 通信示意(P2P 网状)
Master A ◄────────► Master B
│ │
│ │
▼ ▼
Master C ◄────────► Slave B1
│
│
▼
Slave A1
每个节点随机选择其他节点发送 PING,收到 PONG 响应
消息中携带自身状态 + 部分其他节点的信息(Gossip 体)
信息像流言一样传播,最终所有节点达到一致认知3.2 节点维护的信息
每个节点在内存中维护两份核心数据:
// 自身状态——通过 clusterMsg 传播
typedef struct {
char name[40]; // 节点ID(随机生成,40字节hex)
uint64_t currentEpoch; // 当前配置纪元
uint64_t configEpoch; // 节点自身的配置纪元
uint16_t flags; // 节点状态标志(MYSELF/MASTER/SLAVE/FAIL等)
unsigned char slots[2048]; // 负责的槽 bitmap(16384 bits = 2048 bytes)
uint16_t port; // 客户端端口
uint16_t cport; // 集群总线端口(port + 10000)
// ...
} clusterMsg;
// 集群整体状态——维护所有已知节点
typedef struct clusterState {
dict *nodes; // name → clusterNode 的字典
clusterNode *myself; // 指向自身
clusterNode *slots[16384]; // 槽 → 节点映射
rax *slots_to_keys; // 槽 → 属于该槽的 key 数量
// ...
} clusterState;🔬 集群总线端口:每个节点除了正常的客户端连接端口(如 6379),还有一个集群总线端口(port + 10000,如 16379)。节点间的 Gossip 通信、心跳、配置传播都在总线上进行。这个端口必须确保防火墙放行。
3.3 消息类型
Gossip 协议定义了以下消息类型:
| 消息类型 | 方向 | 说明 |
|---|---|---|
| MEET | 发送者 → 新节点 | 邀请一个未知节点加入集群。收到 MEET 的节点会回复 PONG,随后双方定期互相 PING |
| PING | 节点 → 随机节点 | 心跳检测 + 信息同步。携带发送者自身状态和部分其他节点的信息(Gossip 体) |
| PONG | 收到 MEET/PING 的节点 → 发送者 | 响应 MEET 或 PING。也用于节点在状态变更时主动广播(如故障转移后宣布新配置) |
| FAIL | 任意节点 → 所有节点 | 广播某个节点已被确认为 FAIL(不可达下线)。收到 FAIL 的节点立即将该节点标记为 FAIL |
| PUBLISH | 任意节点 → 所有节点 | 集群范围内的发布订阅。客户端在任一台节点上 PUBLISH,消息被广播到所有节点 |
3.4 PING/PONG 通信细节
PING 发送策略
每个节点每秒(cluster-node-timeout 的一半时间)从已知节点列表中随机选取几个节点发送 PING:
PING 选择策略:
1. 选取 5 个随机节点(clusterCron 频率相关)
2. 优先选取:
- 最久未通信的节点(超过 node_timeout/2)
- 统计信息最少的节点
3. 发送 PING 消息,携带:
- 自身状态信息(节点名、槽分配、flags 等)
- 随机选取的 N 个其他节点的信息(Gossip 体)PING 消息结构
// 集群总线消息头
typedef struct {
char sig[4]; // "RCmb" 签名
uint32_t totlen; // 消息总长度
uint16_t type; // 消息类型(MEET/PING/PONG/FAIL/PUBLISH)
uint16_t count; // Gossip 体数量
uint64_t currentEpoch; // 发送者视角的当前纪元
uint64_t configEpoch; // 发送者自身的配置纪元
char sender[40]; // 发送者节点名
unsigned char myslots[2048]; // 发送者负责的槽 bitmap
// ...
// 后面跟着 count 个 Gossip 体
} clusterMsg;
// Gossip 体——描述一个已知节点
typedef struct {
char name[40]; // 目标节点名
uint32_t ping_sent; // 最近一次发送 PING 的时间戳
uint32_t pong_received; // 最近一次收到 PONG 的时间戳
char ip[46]; // 目标节点 IP
uint16_t port; // 目标端口
uint16_t flags; // 目标节点状态标志
} clusterMsgDataGossip;每次 PING 携带的 Gossip 体数量:
Gossip 体选择规则:
- 显示列出:最近通信过的节点
- 再加:随机从已知节点中挑选补齐
- 总数:min(已知节点总数 / 10, cluster_max_gossip_msg_size)3.5 🔬 收敛速度与带宽权衡
Gossip 协议的收敛速度与集群规模和节点超时时间密切相关:
| 因素 | 影响 |
|---|---|
| 节点数量 | 节点越多 → PING 消息越多 → 带宽消耗越大。在 100 节点集群中,每秒约产生 100 次 PING 交互 |
| Gossip 体数量 | 每次 PING 携带的 Gossip 体数量 = 节点总数 / 10。节点越多,每条消息越大 |
| cluster-node-timeout | 超时越短 → PING 越频繁 → 故障检测越快 → 带宽消耗越大。默认 15 秒是较均衡的设定 |
| 故障检测延迟 | 从 master 真的宕机到被标记 FAIL 的延迟 ≈ node_timeout * 1.5 到 node_timeout * 2 |
带宽消耗估算(100 节点集群):
每个 PING 消息:
- 消息头:~104 bytes
- 自身槽 bitmap:2048 bytes
- Gossip 体:100/10 = 10 个,每个 ~104 bytes
- 单条消息总大小:≈ 3 KB
每秒发送频率:每个节点每秒 ~1 次 PING
每秒集群总流量:100 节点 × 3 KB × 2(PING+PONG)= ~600 KB/s
对于千兆网络(125 MB/s),Gossip 开销微不足道
但在万节点场景下,PING 风暴会显著消耗带宽⚡ 性能提示:在小规模集群(< 30 节点)中,Gossip 开销可以忽略。但集群规模超过 200 节点时,应考虑适当增加
cluster-node-timeout以减少 PING 频率,降低带宽压力。
4. 搭建集群
4.1 配置文件
每个节点的 redis.conf 至少需要以下配置:
# ===== Redis Cluster 核心配置 =====
# 开启集群模式(默认 no)
cluster-enabled yes
# 集群节点信息配置文件(自动维护,不要手动编辑)
# Redis 启动时读取该文件恢复集群状态
# 运行时节点变更会自动写回该文件
cluster-config-file nodes.conf
# 节点超时时间(毫秒)
# 节点在这个时间内无响应(无 PONG),被判定为 PFAIL(疑似下线)
# 默认 15000ms(15秒)
cluster-node-timeout 15000
# 是否要求全部 16384 个槽都被覆盖(默认 yes)
# yes:任何槽未分配时集群整体不可用(返回 CLUSTERDOWN 错误)
# 推荐生产环境设为 no:未覆盖的槽不参与服务,但已覆盖的槽正常工作
cluster-require-full-coverage no
# 当一个 master 只剩一个 slave 时,其他有空闲 slave 的 master
# 最多可以把多少 slave 迁移给这个 master。
# 默认为 1:某个 master 有 >(1+1) 个 slave 时,多余的会被考虑迁移
# 设为 0 禁用自动副本迁移
cluster-migration-barrier 1
# 从节点有效性因子(默认 10)
# slave 与 master 断开连接的时间超过:
# node_timeout * cluster-replica-validity-factor
# 则该 slave 不被考虑参与故障转移选举
# 例如:15000 * 10 = 150000ms = 2.5 分钟
cluster-replica-validity-factor 10
# 从节点服务读请求(默认 yes)
# no:slave 将所有请求重定向到 master
# yes:允许客户端通过 READONLY 命令在 slave 上读
cluster-slave-serve-stale-data yes
# 是否允许 slave 在 FAIL 切换时进行数据比对
# yes:需要保证 slave 数据与 master 完全一致
# no:跳过一致性校验
cluster-slave-no-failover no4.2 创建集群(Redis 5.0+)
Redis 5.0 起推荐使用 redis-cli --cluster 命令管理集群,替代了旧的 redis-trib.rb(Ruby 脚本)。
前置准备:先在 6 台机器(或 6 个端口)上启动 Redis 实例,每个配置好 cluster-enabled yes。
# 在每台机器上启动 Redis(以端口 6379 为例)
redis-server /etc/redis/redis.conf创建 3 主 3 从集群:
# --cluster-replicas 1:每个 master 配 1 个 slave
# 前 3 个节点自动成为 master,后 3 个为 slave
redis-cli --cluster create \
192.168.1.1:6379 \
192.168.1.2:6379 \
192.168.1.3:6379 \
192.168.1.4:6379 \
192.168.1.5:6379 \
192.168.1.6:6379 \
--cluster-replicas 1执行后 CLI 会显示槽分配方案,输入 yes 确认:
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 192.168.1.4:6379 to 192.168.1.1:6379
Adding replica 192.168.1.5:6379 to 192.168.1.2:6379
Adding replica 192.168.1.6:6379 to 192.168.1.3:6379
>>> Trying to optimize slaves allocation for anti-affinity
[OK] Perfect anti-affinity obtained!
M: ... 192.168.1.1:6379 slots:[0-5460] (5461 slots) master
M: ... 192.168.1.2:6379 slots:[5461-10922] (5462 slots) master
M: ... 192.168.1.3:6379 slots:[10923-16383] (5461 slots) master
S: ... 192.168.1.4:6379 replicates 192.168.1.1:6379
S: ... 192.168.1.5:6379 replicates 192.168.1.2:6379
S: ... 192.168.1.6:6379 replicates 192.168.1.3:6379
Can I set the above configuration? (type 'yes' to accept): yes💡 最佳实践:
--cluster-replicas 1确保每个 master 至少有一个 slave。如果节点数为奇数,最后一个 slave 会被随机分配给某个 master。建议生产环境使用 3 主 3 从起步。
4.3 查看集群状态
# 1. 查看所有节点信息(最常用)
redis-cli -p 6379 cluster nodes
# 输出每一列的含义:
# <节点ID> <IP:端口@总线端口> <flags> <master_id> <ping_sent> <pong_recv>
# <config_epoch> <连接状态> <槽范围...>
#
# flags 常见值:
# myself — 当前连接的节点
# master — 主节点
# slave — 从节点
# fail? — 疑似下线(PFAIL)
# fail — 已确认下线(FAIL)
# handshake — 正在握手加入集群
# nofailover— 该 slave 不参与故障转移选举
# 2. 查看槽分配(以 slot 范围组织,更直观)
redis-cli -p 6379 cluster slots
# 返回每个槽范围对应的 master 及其 slaves:
# 1) 1) (integer) 0 # 起始槽
# 2) (integer) 5460 # 结束槽
# 3) 1) "192.168.1.1" # master IP
# 2) (integer) 6379 # master 端口
# 3) "node_id..." # master 节点 ID
# 4) 1) "192.168.1.4" # slave IP
# 2) (integer) 6379 # slave 端口
# 3) "node_id..." # slave 节点 ID
# 3. 查看集群整体状态
redis-cli -p 6379 cluster info
# 关键输出字段:
# cluster_state:ok — ok 表示正常,fail 表示集群不可用
# cluster_slots_assigned:16384 — 已分配的槽数
# cluster_slots_ok:16384 — 正常服务的槽数
# cluster_slots_pfail:0 — 疑似下线的槽数
# cluster_slots_fail:0 — 已确认下线的槽数
# cluster_known_nodes:6 — 集群已知节点总数
# cluster_size:3 — 集群 master 数量
# cluster_current_epoch:6 — 当前纪元
# cluster_my_epoch:2 — 本节点配置纪元
# 4. 检查集群健康(全量检查 + 槽一致性校验)
redis-cli --cluster check 192.168.1.1:6379
# 输出示例:
# 192.168.1.1:6379 (f3cb...) -> 0 keys | 5461 slots | 1 slaves.
# 192.168.1.2:6379 (a1b2...) -> 0 keys | 5462 slots | 1 slaves.
# 192.168.1.3:6379 (c3d4...) -> 0 keys | 5461 slots | 1 slaves.
# [OK] 0 keys in 3 masters.
# [OK] All nodes agree about slots configuration.
# [OK] All 16384 slots covered.4.4 连接集群
# -c 参数开启集群模式:自动处理 MOVED/ASK 重定向
redis-cli -c -h 192.168.1.1 -p 6379
# 示例:写入 key 到错误的节点时自动重定向
192.168.1.1:6379> SET user:1001 "Alice"
# -> Redirected to slot [12250] located at 192.168.1.3:6379
# OK
192.168.1.3:6379> GET user:1001
# "Alice"
# 不带 -c 的连接:遇到 MOVED 不会自动重定向,直接报错
redis-cli -h 192.168.1.1 -p 6379
192.168.1.1:6379> GET user:1001
# (error) MOVED 12250 192.168.1.3:6379批量操作:在所有节点上执行命令:
# 在所有节点上执行 INFO server 命令
redis-cli --cluster call 192.168.1.1:6379 INFO server
# 在所有节点上执行 FLUSHALL(危险!)
redis-cli --cluster call 192.168.1.1:6379 FLUSHALL
# 在所有节点上查看慢日志
redis-cli --cluster call 192.168.1.1:6379 SLOWLOG GET 55. 集群伸缩
5.1 扩容:添加节点
集群运行时需要扩容(增加新 master 或 slave),无需停机。
步骤 1:启动新节点
# 在新机器上,确保 redis.conf 中配置了 cluster-enabled yes
redis-server /etc/redis/redis.conf步骤 2:将新节点加入集群
# 语法:redis-cli --cluster add-node <新节点IP:端口> <集群中任意已有节点IP:端口>
redis-cli --cluster add-node 192.168.1.7:6379 192.168.1.1:6379执行后新节点以 master 身份加入集群,但当前负责 0 个槽——它还不能服务数据。
步骤 3:为新 master 分配槽(reshard)
# 将部分槽从现有节点迁移到新节点
redis-cli --cluster reshard 192.168.1.1:6379交互式引导:
>>> How many slots do you want to move (from 1 to 16384)?
输入要移动的槽数量,例如 4096
>>> What is the receiving node ID?
输入目标节点的 ID(新节点),例如 192.168.1.7:6379 的节点 ID
>>> Please enter all the source node IDs.
Type 'all' to use all the nodes as source nodes for the hash slots.
Type 'done' once you entered all the source nodes IDs.
Source node #1:
输入要从哪些节点迁出槽,可以逐个输入,最后输入 done
或输入 all 从所有现有 master 平均迁出⚡ 性能提示:reshard 过程是在线的,不中断服务。但如果迁移大量槽(如迁移全部 16384 个槽的一半),迁移过程会显著消耗带宽和 CPU。建议在业务低峰期执行大规模 reshard。
步骤 4:添加从节点
# 方式一:指定 master 节点 ID
redis-cli --cluster add-node \
192.168.1.8:6379 \
192.168.1.1:6379 \
--cluster-slave \
--cluster-master-id <master_node_id>
# 方式二:让 cluster 自动选择 master 较少的节点
redis-cli --cluster add-node \
192.168.1.8:6379 \
192.168.1.1:6379 \
--cluster-slave在线 rebalance(自动平衡槽分布)
# 自动将槽从槽较多的节点迁移到槽较少的节点
redis-cli --cluster rebalance 192.168.1.1:6379
# 带阈值参数:当某节点的槽数与平均值差异超过 threshold% 时才迁移
redis-cli --cluster rebalance --cluster-threshold 5 192.168.1.1:6379
# 为每个 master 添加独立权重
redis-cli --cluster rebalance --cluster-weight <node1_id>=1.0 <node2_id>=2.0 ...5.2 缩容:下线节点
步骤 1:迁移槽
# 先将要下线节点的槽迁移到其他节点
redis-cli --cluster reshard 192.168.1.1:6379
# 交互式输入:
# 移动槽数 = 该节点当前持有的槽数(全部迁出)
# 目标节点 = 接收槽的节点 ID
# 源节点 = 要下线的节点 ID
# 输入 done 完成源节点选择或者使用 --cluster-from / --cluster-to 参数一次性指定:
redis-cli --cluster reshard 192.168.1.1:6379 \
--cluster-from <下线节点ID> \
--cluster-to <接收节点ID> \
--cluster-slots <槽数量> \
--cluster-yes步骤 2:删除节点
# 语法:redis-cli --cluster del-node <集群中任意节点IP:端口> <要删除的节点ID>
redis-cli --cluster del-node 192.168.1.1:6379 <node_id_to_remove>🚨 重要顺序:先下线 slave,再下线 master(确保 master 的槽已全部迁出)。直接删除一个持有槽的 master 会导致对应的槽无法服务。
5.3 🔬 槽迁移流程(在线 reshard 原理)
单个槽的迁移是 Redis Cluster 中最精妙的部分。迁移过程是逐 key 进行的,期间槽的状态经历多个阶段:
槽 N 的迁移流程:
阶段 1:准备阶段
┌─────────────────────────────────────────────────────────┐
│ 管理员执行 CLUSTER SETSLOT N MIGRATING <目标节点ID> │
│ → 源节点将槽 N 标记为 MIGRATING 状态 │
│ 管理员执行 CLUSTER SETSLOT N IMPORTING <源节点ID> │
│ → 目标节点将槽 N 标记为 IMPORTING 状态 │
└─────────────────────────────────────────────────────────┘
│
▼
阶段 2:逐 key 迁移
┌─────────────────────────────────────────────────────────┐
│ 使用 CLUSTER GETKEYSINSLOT N <count> 获取槽中 key 列表 │
│ 对每个 key 执行 MIGRATE 命令: │
│ MIGRATE <目标IP> <目标端口> <key> 0 <timeout> │
│ │
│ 迁移期间: │
│ - 如果 key 在源节点 → 正常处理 │
│ - 如果 key 已迁走 → 源节点返回 ASK 重定向到目标节点 │
│ - 如果 key 在目标节点且客户端发了 ASKING → 目标节点处理 │
│ │
│ 重复直到槽中所有 key 迁移完毕 │
└─────────────────────────────────────────────────────────┘
│
▼
阶段 3:广播新归属
┌─────────────────────────────────────────────────────────┐
│ 管理员执行 CLUSTER SETSLOT N NODE <目标节点ID> │
│ → 在所有节点上执行 │
│ → 所有节点更新槽 N → 目标节点的映射 │
│ → 通过 Gossip 协议传播到全集群 │
└─────────────────────────────────────────────────────────┘MIGRATE 命令细节
# MIGRATE 是一个原子命令:从源节点 DUMP key → 网络传输 → 目标节点 RESTORE
# 格式:
MIGRATE <host> <port> <key|""> <destination-db> <timeout> [COPY] [REPLACE] [KEYS key1 key2...]
# 示例:迁移单个 key
MIGRATE 192.168.1.7 6379 "user:1001" 0 5000
# 示例:迁移多个 key(一个 MIGRATE 调用迁移一批 key,减少网络往返)
MIGRATE 192.168.1.7 6379 "" 0 5000 KEYS user:1001 user:1002 user:1003🔬 MIGRATE 的原子性:
MIGRATE内部先执行DUMP(序列化 key 的值),将序列化数据发送到目标节点执行RESTORE,成功后源节点执行DEL。整个过程在源节点的单线程中完成,期间该 key 是被阻塞的。如果 key 很大(如含百万元素的 List),MIGRATE也会阻塞源节点,效果类似大 key 的DEL。
6. 故障转移
6.1 自动故障转移
Redis Cluster 的故障检测和自动转移是一个多阶段过程,核心参数是 cluster-node-timeout。
故障检测流程
故障检测全流程:
时间线:
T0: Master A 宕机
│
│ (其他节点持续向 Master A 发送 PING,但收不到 PONG)
│
T0 + node_timeout/2:
│ Master A 被标记为 PFAIL(疑似下线)
│ 每个节点独立判断,只在自身视角内有效
│
│ (Gossip 传播:各节点在 PING/PONG 的 Gossip 体中交换 PFAIL 信息)
│
T0 + node_timeout:
│ 收集到半数以上 master 确认 PFAIL
│ → Master A 被标记为 FAIL(确认下线)
│ → 标记节点向全集群广播 FAIL 消息
│ → 所有收到 FAIL 的节点立即标记 Master A 为 FAIL
│
│ Slave A 检测到自己的 master 被标记为 FAIL
│ → 开始选举流程
│
T0 + node_timeout + 选举时间 + 配置更新:
Slave A 成功当选 new master,广播 PONG 宣布接管槽选举过程
Slave A(需要选举) 其他 Master 节点
1. 发现 master FAIL
│
2. 增加 currentEpoch
│
3. 广播 FAILOVER_AUTH_REQUEST ──────────► 4. 收到请求
(附带 currentEpoch) │
│
5. 检查条件:
✓ 该 master 标记为 FAIL
✓ 本 epoch 还未投票
✓ 该 slave 数据是最新的
│
6. 收集投票 ◄─────────────────── 7. 投票:FAILOVER_AUTH_ACK
8. 当收到 N/2+1 票(半数以上 master 投票)
→ 升级为 master
│
9. 广播 PONG(附带新 configEpoch + 接管槽信息)
│
│ 所有节点收到 PONG 后:更新槽映射 → Slave A 接管原 master 的槽选举关键规则:
| 规则 | 说明 |
|---|---|
| 只会延迟,不会领先 | slave 检测到 master FAIL 后不会立即选举。它会等待一个延迟(500ms + random(0~500ms)),数据越新的 slave 延迟越短,以此确保数据最全的 slave 优先获得投票 |
| 每个 epoch 只投一票 | 每个 master 在每个 epoch 最多投一票,保证不会选出两个新 master(脑裂防护) |
| 需要半数以上 | slave 必须收到超过半数 master 的赞成票才能当选 |
| 数据偏移量 | 只有与旧 master 数据差距最小的 slave 才会参与选举。如果 slave 的 offset 落后太多,会被判定为不合格(cluster-replica-validity-factor 控制) |
🚨 cluster-node-timeout 的影响:超时过短(如 3 秒)→ 网络抖动可能触发误切换,导致频繁主从切换。超时过长(如 60 秒)→ 真正的故障发现太慢,服务中断时间长。15 秒是官方推荐的平衡值。
6.2 手动故障转移
手动故障转移用于计划内维护(如升级 master 的版本、硬件更换),让 slave 主动接管 master 的角色。
# 在要提升的 slave 上执行:
# 方式一:安全故障转移(默认)
# slave 与 master 同步数据 → 确认 offset 一致 → 暂停客户端写入 → 执行切换
CLUSTER FAILOVER
# 方式二:强制故障转移
# 不验证 offset,直接执行切换。适用于 master 已不可达但未被标记 FAIL 的场景
CLUSTER FAILOVER FORCE
# 方式三:接管
# 不进行任何验证和协商,立即接管。适用于 master 确实已宕机且不需要数据一致性
# ⚠️ 最危险的方式——可能丢失尚未同步到 slave 的数据
CLUSTER FAILOVER TAKEOVER手动故障转移流程详解(CLUSTER FAILOVER 默认模式)
Slave Master
CLUSTER FAILOVER
│
1. 向 master 发送 CLUSTERMSG_TYPE_MFSTART
│────────────────────────────────────────►
│ 2. master 收到
│ 设置 clients_pause_end_time
│ 停止接受客户端写入
│
3. slave 持续检查 offset
直到自己的 offset == master offset
(数据完全同步)
│
4. 发起选举(逻辑同自动故障转移)
增加 currentEpoch、请求投票
│
5. 获得多数票 → 升级为 master
│
6. 广播 PONG(宣布接管槽)
│
▼
New Master(原 Slave)💡 最佳实践:计划内维护建议使用
CLUSTER FAILOVER(默认安全模式)。只有在 master 确实无法响应但又未自动切换时才用FORCE。TAKEOVER仅适用于灾难恢复——你可能愿意接受数据丢失来快速恢复服务。
6.3 副本迁移(Replica Migration)
副本迁移是 Redis Cluster 的一个自动保护机制:当某个 master 的所有 slave 都宕机时,从其他 master 的"多余" slave 中自动迁移一个过来。
副本迁移示意:
迁移前:
Master A ──── Slave A1, Slave A2 (Master A 有 2 个 slave → 多余)
Master B ──── 无 slave (Master B 没有 slave → 风险)
迁移后(Slave A2 自动迁移到 Master B):
Master A ──── Slave A1
Master B ──── Slave A2配置参数
| 参数 | 默认值 | 说明 |
|---|---|---|
cluster-migration-barrier |
1 |
迁移屏障。一个 master 的 slave 数量 > (1 + barrier) 时,多出的 slave 才可能被迁移。设为 0 禁用自动迁移 |
cluster-allow-replica-migration |
yes(隐含) | 是否允许自动副本迁移 |
# 示例:如果希望 master 至少保留 2 个 slave,设 barrier=1
# 即 master 有 3 个 slave 时,最多迁移走 1 个
cluster-migration-barrier 1
# 不希望自动迁移:
cluster-migration-barrier 0🔬 副本迁移的意义:当一个 master 变成"孤儿"(所有 slave 不可用)时,如果该 master 宕机,数据将永久丢失。副本迁移通过动态调整 slave 分配,确保每个 master 都尽可能有至少一个 slave 做备份。
7. Hash Tag
7.1 作用
Hash Tag 是 Redis Cluster 提供的一种机制,允许你将多个相关的 key 强制分配到同一个哈希槽(即同一个节点)。这对于需要跨 key 原子操作的场景至关重要。
7.2 规则
如果 key 中包含 {...} 模式,Redis 仅使用花括号内的内容来计算哈希槽:
标准计算:CRC16(整个key) % 16384
Hash Tag:CRC16(花括号内容) % 16384规则详解:
| key | 用于计算槽的内容 | 说明 |
|---|---|---|
{user:1000}.following |
user:1000 |
第一个 {} 内的内容 |
{user:1000}.followers |
user:1000 |
同上,两个 key 必然在同一槽 |
user:{1000}:profile |
1000 |
只有 1000 参与计算 |
user:1000 |
user:1000 |
无 {},整个 key 参与计算 |
foo{}{bar} |
foo{}{bar} |
{} 之间无字符,整个 key 参与计算(Tag 无效) |
{user:1000} |
user:1000 |
整个 key 被花括号包围,等于无 Tag |
匹配规则:
- 从左向右扫描,取第一个
{和第一个}之间的内容 {和}之间必须有至少 1 个字符,否则 Tag 无效- 如果 key 中有多组
{},只取第一组
7.3 💡 应用场景
# 场景 1:用户关注和粉丝存在同一节点
SET {user:1000}:following "Set of user IDs"
SET {user:1000}:followers "Set of user IDs"
# 场景 2:多字段原子更新(事务)
MULTI
SET {user:1000}:name "Alice"
SET {user:1000}:email "alice@example.com"
SET {user:1000}:age 25
EXEC
# 场景 3:Lua 脚本操作多个 key
EVAL "
redis.call('SET', KEYS[1], ARGV[1])
redis.call('SET', KEYS[2], ARGV[2])
return 'OK'
" 2 {user:1000}:profile {user:1000}:settings "Alice" "dark"
# 场景 4:MSET/MGET 批量操作
MSET {user:1000}:name "Alice" {user:1000}:age "25" {user:1000}:city "Beijing"
MGET {user:1000}:name {user:1000}:age {user:1000}:city7.4 🚨 Hash Tag 的风险
| 风险 | 说明 | 后果 |
|---|---|---|
| 数据倾斜 | 大量 key 使用同一个 Tag → 全部集中在同一个槽 → 全部落在一个节点上 | 该节点内存压力大、CPU 繁忙,其他节点资源闲置 |
| 热点节点 | 热点业务的 key 因 Hash Tag 集中在一个节点上 | 单节点成为瓶颈,集群扩展失去意义 |
| 槽不均匀 | 不合理的 Tag 设计导致槽的 key 数量差异极大 | 内存和负载分布不均 |
| 迁移困难 | 某 Tag 下的数据量太大时,槽迁移时间极长 | 扩容和维护变复杂 |
💡 最佳实践:Hash Tag 应仅用于确实需要原子操作的多 key 场景,并确保单 Tag 下的数据量在可控范围内。不要为了"设计好看"而滥用 Hash Tag。如果只是想要逻辑分组,用 key 前缀即可(
user:1000:profile),不需要写成{user:1000}:profile。
数据倾斜示例:
假设一个电商系统,所有订单操作都使用 {order}:xxxx 作为 Tag:
所有 {order}:* 的 key
│
▼
槽 12345 ──► Master B (10GB 内存,80% CPU)
│
其他槽 ────► Master A (2GB 内存,10% CPU)
Master C (2GB 内存,10% CPU)
结果:Master B 成为瓶颈,集群意义丧失8. 集群命令参考
8.1 集群信息查询
| 命令 | 说明 |
|---|---|
CLUSTER NODES |
列出集群中所有已知节点及其状态、槽分配 |
CLUSTER SLOTS |
以槽范围为单位列出 master/slave 分配 |
CLUSTER INFO |
集群状态概要(state、槽覆盖、节点数、纪元等) |
CLUSTER MYID |
返回当前连接节点的 ID |
CLUSTER KEYSLOT <key> |
返回指定 key 所在的槽号 |
CLUSTER COUNTKEYSINSLOT <slot> |
返回指定槽中 key 的数量(仅当前节点) |
CLUSTER GETKEYSINSLOT <slot> <count> |
返回指定槽中的 key 名称(仅当前节点) |
CLUSTER LINKS |
返回当前节点与其他所有节点的连接信息(7.0+) |
# 示例:查看 key 属于哪个槽
CLUSTER KEYSLOT user:1001
# (integer) 12250
# 示例:查看某个槽有多少 key
CLUSTER COUNTKEYSINSLOT 12250
# (integer) 42
# 示例:查看某个槽的具体 key
CLUSTER GETKEYSINSLOT 0 10 # 查看槽 0 中的前 10 个 key8.2 集群管理命令
| 命令 | 说明 |
|---|---|
CLUSTER MEET <ip> <port> |
将当前节点与目标节点握手,使其加入集群 |
CLUSTER FORGET <node_id> |
将指定节点从当前节点移除(60 秒隔离期,之后生效) |
CLUSTER REPLICATE <master_id> |
将当前节点设为指定 master 的 slave |
CLUSTER RESET [SOFT|HARD] |
重置集群状态。SOFT 保留当前纪元,HARD 清空一切 |
CLUSTER FAILOVER [FORCE|TAKEOVER] |
手动触发故障转移 |
CLUSTER SET-CONFIG-EPOCH <epoch> |
手动设置节点配置纪元(用于管理操作) |
# 手动加入集群(不含 redis-cli --cluster 的情况)
CLUSTER MEET 192.168.1.7 6379
# 将一个 slave 重新分配给不同的 master
CLUSTER REPLICATE <new_master_id>
# 彻底重置当前节点(退出集群)
CLUSTER RESET HARD8.3 槽管理命令
| 命令 | 说明 |
|---|---|
CLUSTER ADDSLOTS <slot> [slot ...] |
将槽分配给当前节点 |
CLUSTER DELSLOTS <slot> [slot ...] |
从当前节点移除槽 |
CLUSTER SETSLOT <slot> MIGRATING <node_id> |
标记槽正在从当前节点迁出到目标节点 |
CLUSTER SETSLOT <slot> IMPORTING <node_id> |
标记槽正从源节点迁入当前节点 |
CLUSTER SETSLOT <slot> NODE <node_id> |
将槽正式分配给指定节点(迁移的最后一步) |
CLUSTER SETSLOT <slot> STABLE |
取消槽的 MIGRATING/IMPORTING 状态(回滚) |
CLUSTER FLUSHSLOTS |
清空当前节点负责的所有槽(危险) |
CLUSTER BUMPEPOCH |
递增当前节点的配置纪元 |
# 手动将槽 0-99 分配给当前节点(生产环境建议用 redis-cli --cluster 工具)
CLUSTER ADDSLOTS 0 1 2 3 ... 99
# 标记槽迁移
CLUSTER SETSLOT 12250 MIGRATING <target_node_id> # 在源节点执行
CLUSTER SETSLOT 12250 IMPORTING <source_node_id> # 在目标节点执行
# 迁移完成后,在所有节点上更新归属
CLUSTER SETSLOT 12250 NODE <target_node_id>8.4 读写路由命令
| 命令 | 说明 |
|---|---|
READONLY |
在当前连接上将 slave 设为只读模式(允许从 slave 读数据) |
READWRITE |
恢复 slave 的默认行为(将读请求重定向到 master) |
# slave 节点上默认不能读(客户端会被重定向到 master)
# 使用 READONLY 允许从 slave 读取(可能读到旧数据)
redis-cli -c -h slave_host -p 6379
> READONLY
> GET user:1001 # 直接从 slave 读取,不重定向💡 最佳实践:在 slave 上执行
READONLY可以分担 master 的读压力,但需接受可能读到旧数据的一致性延迟。适用于对实时性要求不高的查询场景。
8.5 其他
| 命令 | 说明 |
|---|---|
CLUSTER BUMPEPOCH |
递增配置纪元(触发配置传播) |
9. 集群限制
9.1 功能限制详解
| 限制 | 说明 | 示例 |
|---|---|---|
| 批量操作需同槽 | MSET、MGET、DEL key1 key2... 等涉及多个 key 的命令,所有 key 必须属于同一槽 |
MSET {order:1}:status paid {order:1}:time "..." |
| 事务只能同节点同槽 | MULTI/EXEC 块中的所有命令涉及的 key 必须在同一槽 |
所有 key 用同一个 Hash Tag |
| Lua 脚本同槽 | EVAL 脚本中操作的所有 KEYS 必须哈希到同一槽。参数 ARGV 不限制 |
EVAL "..." 2 {user:1}:a {user:1}:b |
| 仅支持 db 0 | SELECT 命令在 cluster 模式下会报错 |
用 key 前缀代替多数据库:app1:key、app2:key |
| WATCH 同槽 | WATCH 监视的所有 key 必须在同一槽 |
配合事务使用时注意 |
| SORT 同槽 | SORT 的 key 和 BY/GET 引用的 key 可能不在同一节点,需注意 |
复杂 SORT 操作建议在应用层完成 |
9.2 大 key 迁移阻塞
# 当某个 key 包含大量元素时,MIGRATE 会阻塞源节点:
# - 百万元素的 List:迁移耗时数秒
# - 千万字段的 Hash:迁移耗时数十秒
# 原因:MIGRATE 在源节点的单线程中执行 DUMP(key) → 发送 → 目标 RESTORE → 源 DEL(key)
# 整个过程是同步阻塞的💡 应对方案:定期使用
redis-cli --bigkeys扫描大 key,在 reshard 前先拆分或删除。对于无法删除的大 key,选择业务低峰期执行迁移,并做好监控。
9.3 带宽消耗
集群带宽消耗 = 节点数 × PING 频率 × PING 包大小
100 节点集群示例:
每秒 PING 数:约 100 次
每次 PING + PONG:约 6KB
集群总流量:≈ 600 KB/s(内网可忽略)
500 节点集群:
每秒 PING 数:约 500 次
每次 PING + PONG:约 25KB(Gossip 体更多)
集群总流量:≈ 12.5 MB/s(需考虑,特别是在万兆以下网络)9.4 pub/sub 跨节点广播
# Cluster 模式下的 PUBLISH 会广播到所有节点
# 所有节点的订阅者都能收到消息
# 但——老版本(7.0 之前)的 SUBSCRIBE 不会被路由到集群
# 即:在每个节点上分别 SUBSCRIBE 同一个频道
# 7.0+ 引入 Sharded Pub/Sub:
# 使用 SSUBSCRIBE / SPUBLISH 时,消息按频道名哈希到特定槽
# 只有负责该槽的节点才处理,避免全网广播10. 💡 集群部署建议
10.1 节点规模
| 建议 | 理由 |
|---|---|
| 至少 3 主 3 从 | 3 个 master 是选举投票的最低要求(需半数以上确认 FAIL),每个 master 至少 1 个 slave |
| 单节点内存 < 20GB | 控制故障时 RDB 加载时间(20GB RDB 约需 2-3 分钟),减少全量同步时间 |
| 不要超过 1000 节点 | 官方推荐的集群上限,Gossip 协议在此规模内表现良好 |
10.2 部署拓扑
推荐的跨机架/跨可用区部署:
机架 A(或可用区 A) 机架 B(或可用区 B) 机架 C(或可用区 C)
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Master A │ │ Master B │ │ Master C │
│ Slave C (副本) │ │ Slave A (副本) │ │ Slave B (副本) │
└──────────────────┘ └──────────────────┘ └──────────────────┘
关键原则:每个 master 和它的 slave 不要部署在同一台物理机/同一个机架
这样任何一个机架故障,集群仍然完整(因为 slave 在其他机架上)10.3 配置建议
| 配置项 | 建议值 | 理由 |
|---|---|---|
cluster-node-timeout |
15000(默认) | 15 秒是故障检测速度和误判率之间的平衡点。网络特别不稳定时可加到 30-60 秒 |
cluster-require-full-coverage |
no | 设为 yes 时,任意一个槽不可用则整个集群拒绝服务(CLUSTERDOWN)。生产强烈建议 no |
cluster-migration-barrier |
1(默认) | 允许自动副本迁移,确保每个 master 有 slave |
cluster-replica-validity-factor |
10(默认) | slave 断连超过 node_timeout * factor 则不参与选举 |
maxmemory |
根据物理内存设定 | 单节点建议 < 20GB,预留内存给 fork 和缓冲区 |
10.4 客户端选择
| 客户端(Smart Client) | 语言 | 特点 |
|---|---|---|
| JedisCluster | Java | 最广泛使用的 Java Redis Cluster 客户端,自动维护槽映射 |
| Lettuce | Java | 基于 Netty 的异步客户端,支持 Cluster 模式 |
| go-redis | Go | Go 生态首选,完善的 Cluster 支持 |
| redis-py-cluster | Python | Python Cluster 客户端 |
| ioredis | Node.js | Node.js 首选,完善的 Cluster 支持 |
| StackExchange.Redis | C# | .NET 生态首选 |
💡 使用 Smart Client 的核心好处:Smart Client 会缓存
slots → nodes映射表,收到 MOVED 后自动更新缓存。这意味着大多数请求不需要重定向,延迟更低。
10.5 运维清单
| 频率 | 操作 | 命令 |
|---|---|---|
| 每日 | 检查集群状态 | redis-cli --cluster check <任意节点> |
| 每周 | 检查各节点内存使用是否均衡 | redis-cli --cluster info + 各节点 INFO memory |
| 每周 | 扫描大 key | redis-cli --bigkeys |
| 每月 | 备份 nodes.conf 文件 |
每个节点的 cluster-config-file 文件 |
| 变更前 | 在执行 reshard/故障转移前先备份 | CLUSTER NODES 输出保存 |
| 变更后 | 验证集群健康 | redis-cli --cluster check + cluster info |
11. 集群 vs 单机 vs Proxy 方案
全貌对比帮助技术选型:
| 特性 | 单机/主从 | Redis Cluster | Proxy 方案(Codis/Twemproxy) |
|---|---|---|---|
| 扩展性 | 无(垂直扩展) | 原生支持水平扩展 | 支持水平扩展 |
| 客户端复杂度 | 低(直连即可) | 中(需要 Smart Client) | 低(Proxy 屏蔽细节,客户端像单机一样用) |
| 运维复杂度 | 低 | 中(节点管理、槽迁移) | 高(多组件:Proxy + 配置中心 + 存储层) |
| 多 key 操作 | 完全支持 | 有限制(同槽) | 支持(部分方案有限制) |
| 事务支持 | 完全支持 | 有限制(同槽) | 支持(部分方案有限制) |
| Lua 脚本 | 完全支持 | 有限制(同槽) | 支持(部分方案有限制) |
| 故障转移 | 无(单机)/ Sentinel | 内置自动故障转移 | 依赖额外组件(Sentinel 或自定义) |
| 性能开销 | 无额外开销 | PING/PONG Gossip 开销 | Proxy 增加一跳延迟(~0.5-1ms) |
| 成熟度 | 最成熟 | 官方方案,持续迭代 | 第三方项目,更新可能滞后 |
| 社区支持 | 完全 | 全面 | 有限 |
| 适用场景 | 数据量 < 单机内存 | 数据量大、写并发高、需要水平扩展 | 不想改造客户端、需要兼容多 key 操作 |
💡 选型建议:
- 数据量 < 10GB 且写入压力不大 → 单机 + 主从 + Sentinel 足够
- 数据量大或写入压力高、能接受同槽限制 → Redis Cluster
- 需要多 key 原子操作且必须分片、有代理层维护能力 → Codis 或自研 Proxy
- 云环境 → 优先使用云厂商托管服务(阿里云 Redis 集群版 / AWS ElastiCache 等),降低运维负担
12. 🚨 常见陷阱
12.1 忘记 -c 参数导致 MOVED 错误
# 错误:不使用集群模式连接
redis-cli -h 192.168.1.1 -p 6379
> SET user:1001 "Alice"
# (error) MOVED 12250 192.168.1.3:6379
# 正确:加 -c 参数
redis-cli -c -h 192.168.1.1 -p 6379
> SET user:1001 "Alice"
# -> Redirected to slot [12250] located at 192.168.1.3:6379
# OK12.2 跨槽操作默默失败
# 错误:MSET 的 key 分布在不同的槽
MSET user:1 "Alice" product:1 "Book"
# (error) CROSSSLOT Keys in request don't hash to the same slot
# 正确:使用 Hash Tag 将相关 key 约束到同一槽
MSET {app}:user:1 "Alice" {app}:product:1 "Book"事务和 Lua 脚本有同样的问题:
# 错误:Lua 脚本的 KEYS 跨槽
EVAL "return redis.call('MGET', KEYS[1], KEYS[2])" 2 user:1 product:1
# (error) CROSSSLOT Keys in request don't hash to the same slot
# 正确:
EVAL "return redis.call('MGET', KEYS[1], KEYS[2])" 2 {app}:user:1 {app}:product:112.3 cluster-require-full-coverage=true 导致全集群不可用
# 危险配置
cluster-require-full-coverage yes # 默认值
# 后果:哪怕只有一个槽不可用(如 master 宕机还没有 failover),
# 整个集群返回 CLUSTERDOWN,所有读写全部拒绝!# 当某 master 宕机且 failover 未完成时:
> GET any_key
# (error) CLUSTERDOWN The cluster is down
# 正确做法:
cluster-require-full-coverage no
# 只有受影响的槽不可用,其他槽正常服务12.4 在业务高峰期执行 reshard
# 危险:高峰期执行 reshard
# 迁移会消耗带宽、CPU,并导致部分 key 的请求需要 ASK 重定向
# 重定向增多 → 延迟增大 → 客户端超时率飙升
# 正确:在业务低峰期(如凌晨 2-5 点)执行
# 并且使用 --cluster-yes 参数减少交互时间
redis-cli --cluster reshard 192.168.1.1:6379 --cluster-yes12.5 槽分配不均衡导致数据倾斜
# 检查槽分布是否均匀
redis-cli --cluster info 192.168.1.1:6379
redis-cli -p 6379 CLUSTER SLOTS
# 如果发现某个节点负责的槽数明显多于其他节点:
# 重新平衡
redis-cli --cluster rebalance 192.168.1.1:6379
# 如果是因为某个槽中有大量 key(Hot Slot):
# 1. 使用 CLUSTER COUNTKEYSINSLOT 定位热点槽
# 2. 考虑调整 Hash Tag 策略,将热点数据打散12.6 cluster-node-timeout 设置过短导致频繁切换
# 危险:设置过短的超时时间
cluster-node-timeout 3000 # 3秒
# 后果:网络轻微抖动超过 3 秒 → 节点被判定 PFAIL → FAIL →
# → slave 提升为 master → 旧 master 恢复后发现集群已变
# → 主从切换频繁,数据同步开销大,服务不稳定# 推荐:除非网络极好且需要快速故障检测,否则保持默认值
cluster-node-timeout 15000 # 15秒,绝大多数场景适用12.7 节点间时间不同步
# 问题:各节点系统时间差异过大(如差几十秒)
# 影响:
# - PING/PONG 时间戳判断不准确
# - 故障检测可能误判(某些节点认为心跳超时)
# - 日志关联困难
# 解决:所有节点配置 NTP 时间同步
sudo apt install ntp # Debian/Ubuntu
sudo yum install ntp # CentOS/RHEL
sudo systemctl enable --now ntp12.8 大 key 迁移导致阻塞
# Big Key 迁移风险:
# 1. MIGRATE 是同步命令,大 key 迁移时源节点阻塞
# 2. 目标节点 RESTORE 大 key 也会阻塞(单线程)
# 3. 如果 key 太大导致超时,迁移失败
# 提前发现大 key:
redis-cli --bigkeys
redis-cli --memkeys
# 应对:
# 1. 将大 key 拆分成多个小 key(如 List 分页)
# 2. 在低峰期迁移
# 3. 逐个手动 MIGRATE,增加 timeout 参数
MIGRATE 192.168.1.7 6379 "big_hash" 0 30000 # 30秒超时12.9 Hash Tag 滥用导致严重数据倾斜
# 错误:为了方便,所有 key 都加 Hash Tag
SET {global}:user:1 "Alice"
SET {global}:user:2 "Bob"
SET {global}:product:1 "Book"
# ... 数百万 key 全带 {global}
# 后果:所有数据集中在一个槽 → 一个节点
# → 该节点内存爆炸、CPU 打满
# → 其他节点基本空转
# → 集群完全失去意义# 正确:Hash Tag 只用于确实需要原子操作的少量相关 key
# 大多数 key 不加 Hash Tag,让其自然分散
SET user:1 "Alice" # 自然分布
SET user:2 "Bob" # 自然分布
# 只有需要事务/Lua 的 key 才用 Hash Tag
SET {user:1}:profile "..."
SET {user:1}:settings "..."12.10 CLUSTER 模式不支持 SELECT
# 错误:在 Cluster 模式中使用 SELECT
redis-cli -c -h host -p 6379
> SELECT 1
# (error) ERR SELECT is not allowed in cluster mode
# 如果应用代码中使用了 SELECT 做逻辑隔离:
# 1. 将所有数据改用 db 0
# 2. 用 key 前缀做逻辑隔离:app1:key、app2:key
# 3. 修改连接配置,数据库参数设为 012.11 客户端连接单个节点导致单点故障
# 错误:客户端只硬编码连接集群中的一个节点
# 如果这个节点宕机,客户端完全无法工作
# 正确:客户端配置集群中多个(所有 master)节点地址
# Smart Client 会通过任一可用节点获取整个集群拓扑
# go-redis 示例:
clusterClient := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{
"192.168.1.1:6379",
"192.168.1.2:6379",
"192.168.1.3:6379",
"192.168.1.4:6379",
"192.168.1.5:6379",
"192.168.1.6:6379",
},
})12.12 未备份 nodes.conf 文件
# nodes.conf 记录了节点ID、槽分配、已知节点列表等关键信息
# 如果所有节点的 nodes.conf 同时损坏或丢失,集群将无法重建
# 定期备份(建议加入 cron):
cat /var/lib/redis/nodes.conf # 或者其他 cluster-config-file 路径
# 或者通过命令获取:
redis-cli -p 6379 CLUSTER NODES > cluster_backup_$(date +%Y%m%d).txt快速参考卡
集群管理命令速查
| 操作 | 命令 |
|---|---|
| 创建集群 | redis-cli --cluster create <hosts> --cluster-replicas 1 |
| 检查集群 | redis-cli --cluster check <host:port> |
| 集群信息 | redis-cli -p 6379 cluster info |
| 节点信息 | redis-cli -p 6379 cluster nodes |
| 槽分配 | redis-cli -p 6379 cluster slots |
| 添加节点 | redis-cli --cluster add-node <new> <existing> |
| 添加 slave | redis-cli --cluster add-node <new> <existing> --cluster-slave |
| 重新分片 | redis-cli --cluster reshard <host:port> |
| 删除节点 | redis-cli --cluster del-node <host:port> <node_id> |
| 重新平衡 | redis-cli --cluster rebalance <host:port> |
| 修复槽 | redis-cli --cluster fix <host:port> |
| 在所有节点执行 | redis-cli --cluster call <host:port> <command> |
槽命令速查
| 操作 | 命令 |
|---|---|
| 计算 key 所属槽 | CLUSTER KEYSLOT <key> |
| 槽中 key 数量 | CLUSTER COUNTKEYSINSLOT <slot> |
| 获取槽中 key | CLUSTER GETKEYSINSLOT <slot> <count> |
| 手动故障转移 | CLUSTER FAILOVER [FORCE|TAKEOVER] |
| 重置节点 | CLUSTER RESET [SOFT|HARD] |
常用配置速查
| 配置 | 推荐值 |
|---|---|
cluster-enabled |
yes |
cluster-node-timeout |
15000 |
cluster-require-full-coverage |
no |
cluster-migration-barrier |
1 |
cluster-replica-validity-factor |
10 |