跳到正文

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 ​

MOVEDASK
含义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(默认 15000 ms)没收到 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 统计 QPStag 粒度等于业务主键时会放大热点
映射分布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 的客户端行为不同,处理错就丢请求。

MOVEDASK
含义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
缺失的 slotCLUSTER 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 no
text
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 更简单。

把一门技术讲透到既懂也会用