主题
Redis Cluster 集群模式
一句话:Cluster 用 16384 个 hash slot 把数据分片到多个主节点,靠 Gossip 去中心化协调;换来写扩展和自动故障转移,代价是跨 slot 操作受限、客户端必须处理重定向。
适用版本:Redis 7.x / 8.x。7.0 起 Cluster 支持分片 Pub/Sub(SSUBSCRIBE),基础分片机制未变。
为什么需要
主从 + Sentinel 解决了读扩展和自动切换,但没解决写扩展和单机内存上限:所有节点存全量数据,写仍然只有一个主节点。Cluster 把数据按 key 切成 16384 份,分给多个主节点,每个主节点再带副本;写和容量都随主节点数量扩展。
代价是「分片」这件事暴露给了客户端和业务:key 怎么路由、事务能不能跨 key、迁移期间请求怎么处理,都要提前设计。
核心机制
16384 个 hash slot
slot = CRC16(key) % 16384
每个 Master 负责一部分 slot,Slave 复制对应 Master为什么是 16384 而不是 65536:Cluster 节点间 Gossip 心跳要携带 slot 位图,16384 个 bit 是 2 KB,65536 个是 8 KB;心跳频繁发送,2 KB 更省带宽。16384 对通常不超过 1000 个节点的集群也足够均匀。
客户端启动时通过 CLUSTER SLOTS / CLUSTER SHARDS 拉取 slot → 节点映射并缓存,之后直连目标节点,避免每次请求都问一遍。
Hash tag:让相关 key 同 slot
只对 {} 之间的内容计算 hash,同 tag 的 key 必然落进同一个 slot。
这样 MGET、事务、Lua 才能操作这几个 key。代价是 tag 太集中会造成数据倾斜,{1001} 下的数据全落在一个节点。
MOVED 与 ASK
| MOVED | ASK | |
|---|---|---|
| 含义 | slot 已永久归属新节点 | slot 正在迁移,本次去新节点 |
| 客户端行为 | 更新本地 slot 映射 | 不更新映射,仅本次跳转 |
| 请求要求 | 正常重发 | 先发 ASKING,再发命令 |
text
MOVED 5000 10.0.0.2:6379 → 客户端更新映射,之后直连新节点
ASK 5000 10.0.0.2:6379 → 客户端加 ASKING 后临时访问,下次仍问原节点Reshard:按 slot 迁移
迁移由协调方(redis-cli --cluster reshard)驱动:先给两端挂上 IMPORTING/MIGRATING 状态,再按批把 key 搬到目标节点,最后广播 slot 归属变更。
迁移期间:key 还在源节点就正常返回;已经迁走的 key 返回 ASK;新写入直接落目标节点。大 Key 会让单次 MIGRATE 变慢,迁移要避开高峰。
Gossip 与故障转移
节点间通过 MEET、PING、PONG、FAIL 交换元数据:
- PFAIL:节点 A 超过
cluster-node-timeout(默认15000ms)没收到 B 的 PONG,主观认为 B 下线。 - FAIL:A 通过 Gossip 收集到多数主节点都认为 B 是 PFAIL,广播 FAIL,进入客观下线。
- 选主:B 的副本发起选举,向其他主节点拉票;获得多数主节点投票的副本提升为主,接管 B 的 slot。
- epoch:
config epoch全局递增,用于裁决同一 slot 的归属冲突,防止脑裂时两个主节点都声称拥有同一个 slot。
关键限制:故障转移需要多数主节点存活。少数派分区里的副本无法当选,集群会保持不可写,这是有意的保护。
实战场景
一、MGET 报 CROSSSLOT,加了 hash tag 又把一个节点打满
现象:从主从架构迁到 Cluster 后,一次批量取用户标签的 MGET 报 CROSSSLOT Keys in request don't hash to the same slot。开发按文档加上 {uid} 让整批 key 同 slot,接口通了;一周后监控显示某个节点内存与 CPU 明显高于其他节点,且这个节点的 keyspace 集中在少数几个大客户 uid 上。
排查:hash tag 只对 {} 之间的内容算 hash,代价是集中度。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 报错语义 | CROSSSLOT 出现在多 key 命令、MULTI、Lua、RENAME 跨 slot 场景 | 需要原子性时只能同 slot |
| slot 计算 | slot = CRC16(key) % 16384,取 tag 内容 | {1001} 下所有 key 落同一 slot |
| 数据倾斜 | 各节点的 used_memory、db0:keys= 计数差异 | 单节点显著高即倾斜 |
| 热点集中 | redis-cli --hotkeys、按 uid 统计 QPS | tag 粒度等于业务主键时会放大热点 |
| 映射分布 | CLUSTER SLOTS / CLUSTER SHARDS 的 slot 归属 | 某些 slot 集中在一台 Master |
对症做法:
text
1. tag 的粒度按“确实需要原子操作的最小集合”选,不是越大越方便:
user:{1001}:name → 需要同一用户的字段一起读,粒度合理
tag 用大客户 id、常量 → 全量数据压进一个 slot,禁用
2. 不需要原子性的批量读,改成“按 slot 分组并发 + 本地合并”:
客户端按 CRC16 分组,每组发往各自节点,一次网络往返一批 key,
比强行同 tag 更均衡,延迟也接近。
3. 事务/Lua 只在确实需要时用;跨 slot 的“多 key 一致性”要靠业务版本或补偿,
不要靠 tag 把整个业务域塞进一个节点。
4. 倾斜已经发生时:
- 拆 tag(例如把 user:{uid} 改成 user:{uid}:{shard},shard 取 0~7)
- 或对倾斜的 slot 做 reshard,把它挪到容量更大的节点java
// 分组批量读:按节点分组,每组一次 MGET,最后合并结果
Map<Integer, List<String>> bySlot = keys.stream()
.collect(groupingBy(k -> crc16(tagOf(k)) % 16384 / slotPerNode));
bySlot.forEach((node, batch) -> futures.add(pool.submit(() -> conn(node).mget(batch))));复盘要点:判据是“各节点 key 数与内存的标准差”在阈值内(例如变异系数 < 0.3,经验值需校准),以及“单 slot 承载的 QPS 不超过节点上限的一半”。选 Cluster 还是主从的取舍要写清:Cluster 换来容量与并行扩展,代价是跨 slot 原子性受限与运维复杂度,见 双活与故障切换。
二、reshard 期间客户端把 ASK 当 MOVED,请求静默丢失
现象:扩容加了一个节点并做 slot 迁移。迁移窗口里应用日志出现大量 JedisAskDataException/MOVED 重试,客服反馈“刚写入的数据查不到”,甚至有请求直接失败。DBA 确认迁移本身在正常推进。
排查:MOVED 与 ASK 的客户端行为不同,处理错就丢请求。
| MOVED | ASK | |
|---|---|---|
| 含义 | slot 已永久归属新节点 | slot 正在迁移,本次去新节点 |
| 客户端行为 | 更新本地 slot 映射 | 不更新映射,仅本次跳转 |
| 请求要求 | 正常重发 | 先发 ASKING,再发命令 |
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 迁移状态 | 源节点 IMPORTING/MIGRATING 标记与 CLUSTER INFO 的 slot 状态 | 迁移中才可能返回 ASK |
| 是否把 ASK 当 MOVED | 客户端映射更新时间戳 | 见 ASK 就更新映射 → 之后直连新节点,漏掉留在源节点的 key |
是否漏发 ASKING | 命令前是否带 ASKING | 缺失会收到 ASK 后续命令被拒 |
| 客户端版本 | 驱动是否原生支持集群重定向 | 自研封装最容易在这里写错 |
| key 是否已搬完 | 迁移按批进行,同一 key 两次请求可能落在不同节点 | 读旧值窗口属预期 |
把 ASK 当成 MOVED 更新映射,等于认为“这个 slot 已经彻底迁完”,之后所有请求绕过源节点直连——而源节点上还有一批没搬走的 key,表现就是“写进去的数据查不到”。
对症做法:
text
1. 用官方驱动(Jedis/Lettuce 集群模式),不要自己解析 MOVED/ASK;
Lettuce 支持定时刷新 + 拓扑事件推送,能更快跟上 slot 归属变更。
2. 迁移窗口尽量小、按 slot 分批推进:
redis-cli --cluster reshard 时控制每批 slot 数与限速,
迁移越久,ASK 窗口越长、业务影响越大。
3. 迁移期间不要做 failover:slot 处于迁移状态时故障切换会被阻塞,
导致迁移长时间不收敛。
4. 业务侧:迁移窗口内的“写后立刻读”走同一 key 路由(同节点),
避免依赖跨节点即时可见性。bash
# 迁移前先确认目标与源状态,迁移中持续观察收敛
redis-cli -h $SRC cluster slots | head
redis-cli -h $SRC info stats | grep -E 'cluster|migrate'
redis-cli --cluster check $ANY_NODE:6379 # 迁移完成后校验 slot 覆盖是否完整复盘要点:判据是“迁移结束后 --cluster check 无告警,且客户端映射刷新时间晚于迁移完成时间”。迁移期间应用侧的错误率要单列统计(按异常类型区分 ASK/MOVED/超时),混在一起就无法判断是迁移本身还是客户端处理有问题。
三、一个 slot 没覆盖,整个集群拒绝服务
现象:一次节点故障后,Redis Cluster 里只有 3% 的 key 受影响,但应用侧所有命令都返回 CLUSTERDOWN The cluster is down。事后发现配置是 cluster-require-full-coverage 默认值 yes。
排查:全覆盖要求是集群级别的开关。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 集群状态 | CLUSTER INFO 的 cluster_state(ok/fail) | fail 即整体拒绝 |
| 覆盖要求 | CONFIG GET cluster-require-full-coverage | 默认 yes:有 slot 未覆盖就整体 DOWN |
| 缺失的 slot | CLUSTER SLOTS 与 16384 全集比对 | 找出未覆盖区间 |
| 故障域 | 故障节点承载的 slot 区间与占比 | 3% 的 key 引发 100% 拒绝即命中 |
| 切换是否可行 | 是否有对应 Slave、cluster-node-timeout 是否到期 | Slave 齐全才能补齐覆盖 |
| Gossip 开销 | 心跳携带 slot 位图(16384 bit = 2 KB) | 这也是为什么选 16384 而不是 65536 |
对症做法:
properties
# 1. 允许部分 slot 不可用,好过整体不可用(业务需能按 key 降级)
cluster-require-full-coverage notext
2. 前提是每个 Master 都有健康 Slave,否则关了这个开关只是把
“全挂”变成“部分挂”——部分挂对业务同样可能不可接受。
3. cluster-node-timeout 要按“误判 vs 发现速度”权衡:
过小会在网络抖动时频繁切换,过大则故障恢复慢(默认 15s 量级,按压测校准)。
4. 应用侧必须有按 key 的降级路径:
读失败时回落数据库并把该 key 标记为暂不可用,而不是让所有请求一起失败。
5. 容量与故障演练要覆盖“单 Master + 其 Slave 同时故障”这一最坏组合,
这是判断能否容忍关掉全覆盖的真正依据。java
// 按 key 的降级:命令失败只影响落在故障 slot 上的请求
try {
return redis.get(key);
} catch (RedisCommandFailedException | JedisConnectionException e) {
metrics.slotDegraded(key);
return dao.load(key); // 回源 + 可选短 TTL 重建
}复盘要点:判据是“单个 Master 及其 Slave 同时故障时,业务错误率不超过其承载 slot 的占比”。这个结论只能靠演练得到,配置改完不做演练等于没改。Gossip 与心跳的具体行为、故障切换流程见 哨兵模式 与 双活与故障切换。
最小上手
bash
# 起 6 个节点(3 主 3 从)
for p in 7000 7001 7002 7003 7004 7005; do
redis-server --port $p --cluster-enabled yes \
--cluster-config-file nodes-$p.conf --daemonize yes
done
redis-cli --cluster create \
127.0.0.1:7000 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 \
--cluster-replicas 1
redis-cli -c -p 7000 CLUSTER INFO # cluster_state:ok
redis-cli -c -p 7000 SET foo bar
redis-cli -c -p 7000 CLUSTER SLOTS-c 让 redis-cli 自动跟随 MOVED/ASK;不带 -c 时重定向会直接打印出来。
生产动作
- 至少 3 主 3 从:3 个主节点才能在主节点故障时形成多数派;副本要和主节点分机器/分机架。
cluster-node-timeout默认 15000 ms:调小切换快但误判多,调大误判少但故障窗口长。cluster-require-full-coverage默认yes:有 slot 不可用时整个集群拒绝写;可用性优先的场景可设no,但要接受部分 key 不可用。cluster-allow-replica-migration默认yes、cluster-migration-barrier默认1:允许没有副本的主节点从富余副本迁移一个过来,提升容错。- 运维命令:
CLUSTER INFO看cluster_state、cluster_slots_assigned、cluster_slots_pfail/fail;CLUSTER NODES看 slot 归属和迁移状态。 - 扩容/缩容:用
redis-cli --cluster add-node、--cluster reshard、--cluster del-node,迁移在低峰做,先确认大 Key 分布。 - 客户端必须支持 MOVED/ASK:普通单机客户端连 Cluster 会不断报重定向错误。
边界
- 跨 slot 多 key 操作不支持:
MGET、MSET、事务、Lua 都要求 key 在同一 slot,靠 hash tag 解决。 - 只有 db0:Cluster 不支持多数据库,
SELECT不可用;用 key 前缀隔离业务。 - 没有跨节点强事务:每个 slot 内部是单线程原子,跨 slot 只能靠业务补偿。
- 故障转移需要多数主节点:少数派分区不可写;主节点数量少于 3 时容错能力很弱。
- 迁移不是零成本:
MIGRATE会序列化并传输整个 key,大 Key 会阻塞源节点和目标节点。 - 客户端和运维复杂度高:连接池、重定向、slot 缓存、迁移窗口都要处理;小规模场景优先 Sentinel。
讲清楚
Cluster 的核心是把 key 空间切成 16384 个 slot,每个主节点负责一部分,客户端用
CRC16(key) % 16384算出 slot 再直连。节点之间用 Gossip 同步 slot 归属和存活状态,某个主节点被多数主节点判为下线后,它的副本发起选举接管 slot。分片带来的限制很实在:跨 slot 的
MGET、事务、Lua 都不行,要靠 hash tag 把相关 key 放到同一 slot;迁移期间会出现 ASK,客户端必须支持重定向。它适合数据量和写吞吐超过单机的场景;小规模用主从加 Sentinel 更简单。