Redis Cluster 与 Hash Slot 原理
→ 返回 Redis
Redis Cluster 是去中心化的分片与高可用方案:数据按 16384 个 Hash Slot 分给多个主节点,每个主节点可配置从节点;客户端缓存 slot → node 路由表并直接访问目标节点,不需要一个固定代理承担所有请求。
客户端(缓存 slot 路由)
/ | \
Master A Master B Master C
slot 0~5460 5461~10922 10923~16383
│ │ │
Replica A' Replica B' Replica C'
节点间:Cluster Bus + Gossip + 故障检测 + 配置传播redis-cli --cluster create host1:7000 host2:7001 host3:7002 \
host4:7003 host5:7004 host6:7005 --cluster-replicas 1
redis-cli -c -p 7000 CLUSTER SHARDS
redis-cli -c -p 7000 CLUSTER NODESHash Slot 如何计算
普通 key 的槽位:
slot = CRC16_XMODEM(key) mod 16384
= CRC16_XMODEM(key) & 0x3FFFCRC16 产生 16 位结果,Redis Cluster 取低 14 位,因此槽位范围是 0~16383。槽是分片迁移和路由的最小逻辑单位;一个槽包含许多 key,迁移槽实际是逐个迁移属于该槽的 key。
为什么是 16384
这是路由粒度、集群消息大小与实际节点规模之间的工程折中:
- 16384 = 2^14,取模可用位运算;
- 节点可用 16384 bit(约 2 KiB)位图表达自己负责的槽,适合在集群消息中传播;
- 槽数量远大于常见主节点数量,扩容时仍能较均匀地重新分配;
- 槽过多会增加状态传播和管理成本,并不等于能支持同等数量的实用主节点。
Redis 官方建议的实际节点规模远小于 16384;16384 是槽数和理论主节点上限,不是推荐部署规模。
哈希标签 Hash Tag
若 key 包含第一个合法的 {...},只对花括号中的非空内容计算 CRC16:
CLUSTER KEYSLOT user:1
CLUSTER KEYSLOT '{user:1}:profile'
CLUSTER KEYSLOT '{user:1}:orders'
# 后两个 key 都只 hash user:1,因此落在同一 slotHash Tag 让多 key 命令、事务或 Lua 所需 key 落到同槽,但大量 key 使用同一个标签会制造热点,失去分片效果。
客户端路由与 MOVED
Cluster 客户端启动时通过 CLUSTER SHARDS(新客户端优先)或 CLUSTER SLOTS 获取路由表:
GET order:100
│
├─ 客户端计算 slot,命中本地 slot → node 缓存
├─ 直接请求目标节点
└─ 若路由过期,节点返回 -MOVED <slot> <host:port>
│
└─ 客户端刷新映射并重新请求MOVED 表示槽的正式归属已经改变,客户端应更新槽映射。客户端通常会借此刷新整张路由表,因为故障切换或扩容往往同时改变一批槽。
普通 Redis 客户端不一定支持 Cluster;需要使用 JedisCluster、Lettuce Cluster、Redisson Cluster 等能计算槽位、维护拓扑并处理重定向的客户端。
在线迁槽与 ASK
把 slot 8 从 A 迁到 B 时,核心状态为:
A:slot 8 MIGRATING → B
B:slot 8 IMPORTING ← A迁移并非瞬间完成:槽内已有 key 被分批从 A 搬到 B,这段时间同一槽的 key 可能暂时分布在两端。
- A 上 key 仍存在:A 直接执行;
- A 上 key 已迁走或不存在:A 返回
ASK 8 B; - 客户端仅对这一次请求向 B 先发送
ASKING,再执行原命令; - 全部 key 迁完并正式修改槽归属后,后续错误变为
MOVED。
ASK = 临时重定向,不永久修改本地 slot 表
MOVED = 永久重定向,应更新 slot 表迁槽期间多 key 操作更容易遇到 key 分处两端的问题,应在低峰执行、限制迁移速率并关注大 key。迁移单位虽然是槽,但网络传输单位最终仍是 key;一个超大 key 会拖慢迁移。
节点通信与 Gossip
Cluster 节点之间使用独立的集群总线传播:
- 节点身份、角色、配置纪元和槽归属;
PING/PONG心跳与随机节点信息;MEET加入集群;- 主观/客观故障信息与故障转移投票;
PUBLISH等需要集群传播的信息。
Gossip 不要求每次心跳都携带完整拓扑,而是让信息逐步扩散。优点是没有单一元数据中心;代价是拓扑收敛需要时间,网络抖动时各节点可能短暂看到不同状态。
每个节点都有唯一 Node ID,并在本地保存整个集群的槽归属视图。配置纪元用于解决多个节点声称同一槽归属时的新旧冲突。
故障检测与副本提升
某节点长时间无响应
▼
多个节点传播 PFAIL(疑似故障)
▼ 达到主节点多数派判断
FAIL
▼
故障主节点的从节点延迟竞选
▼
主节点按配置纪元投票
▼
获多数票的从节点提升为主并接管原 slots从节点会综合复制进度等条件决定是否有资格参与,避免数据落后太多的副本轻易成为新主。切换期间客户端通过 MOVED 和拓扑刷新学习新路由。
Cluster 仍是异步复制:旧主写入成功但尚未复制的命令可能在切换后丢失;网络分区中的少数派主节点也可能在短窗口内接收最终无法保留的写入。
cluster-node-timeout 影响故障检测与可用性,不应仅为了“切换更快”盲目调小,否则短暂抖动会增加误判和频繁 failover。
可用性与 full coverage
若某个槽没有可用主节点:
- 开启
cluster-require-full-coverage yes时,集群可拒绝全部 key 请求; - 关闭时,仍可继续服务有明确可用归属的其他槽,但应用会得到部分成功、部分失败。
这项选择体现一致性、故障隔离和业务可接受的部分可用之间的权衡。跨槽业务操作尤其要考虑部分失败和补偿。
跨槽限制与事务
节点只能原子地操作自己拥有的数据,所以多 key 命令通常要求所有 key 属于同一槽,否则返回 CROSSSLOT。
| 场景 | 处理方式 |
|---|---|
MGET 多 key | 用 Hash Tag 放同槽,或由客户端按节点拆分并聚合 |
MULTI/EXEC | 涉及的 key 必须同槽;它也不提供跨节点事务 |
| Lua / Function | 声明和访问的 key 应同槽,避免动态访问其他槽 |
| 分布式锁 | 单锁 key 天然单槽;多资源锁需重新设计一致性边界 |
| 批处理 | Pipeline 按节点分组发送,不保证跨节点原子性 |
同槽只解决“能否在一个节点执行”,不自动解决数据库事务意义上的回滚、隔离和业务一致性。
热点、倾斜与大 key
| 问题 | 原因 | 应对 |
|---|---|---|
| 热 key | 单 key 必然只在一个槽和一个主节点 | 本地缓存、请求合并、读副本(接受陈旧)、业务拆 key |
| 热点槽 | 一批高流量 key 碰巧同槽或共用 Hash Tag | 调整 key 标签、重新分散业务 key |
| 数据倾斜 | 少数槽包含大 key 或海量 key | 统计 per-slot key/内存,迁槽再平衡 |
| 大 key | 单 key 无法跨节点自动拆分 | 按业务维度拆分;渐进扫描;UNLINK 异步删除 |
增加主节点不能自动拆散一个热 key;Cluster 的水平扩展单位是 key/slot,不是 Hash field、List 元素或 ZSet member。
诊断命令
CLUSTER INFO
CLUSTER NODES
CLUSTER SHARDS
CLUSTER KEYSLOT mykey
CLUSTER COUNTKEYSINSLOT 1234
redis-cli --cluster check host1:7000
redis-cli --cluster rebalance host1:7000重点监控:槽是否完整覆盖、节点 PFAIL/FAIL、迁槽状态、重定向次数、复制延迟、单节点 QPS/内存、热点 key、大 key 与集群总线网络。
面试结论
Redis Cluster 把 key 空间划分为 16384 个槽,使用 CRC16 的低 14 位定位槽,再由槽路由到主节点。客户端维护槽表并直连节点;MOVED 表示永久归属变化,ASK 表示迁槽期间的一次性重定向。节点通过 Gossip 和配置纪元传播拓扑、检测故障并投票提升副本。Cluster 解决分片与高可用,但不解决跨槽事务、热 key 和异步复制窗口的数据丢失。
相关
- 主从复制与 Sentinel
- Hash、ZSet(一个 key 不会按内部元素分片)
- 分布式锁
- 运维与集成