KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

05 · 集群分片:槽、hash tag、MOVED 与客户端路由表 — keel 龙骨

前四章的方案里,数据始终只有"一份完整副本",受限于单机内存。集群是唯一能把数据分到多台机器上的形态。这一章讲它的分片规则(16384 个槽)、请求送错节点时 Redis 怎么回答(MOVED)、客户端怎么自己算地址,以及上集群必须接受的三个能力损失。

前四章的方案里,数据始终只有"一份完整副本",受限于单机内存。集群是唯一能把数据分到多台机器上的形态。这一章讲它的分片规则(16384 个槽)、请求送错节点时 Redis 怎么回答(MOVED)、客户端怎么自己算地址,以及上集群必须接受的三个能力损失。


一、现场:内存到顶,但是删不动

单机 Redis 到了 28G / 32G,淘汰策略和大 key 都优化过了,剩下 20G 是确实需要常驻的会话与索引。主从复制帮不上忙——每个副本都是全量数据,加副本只会让"内存占用 × N"。哨兵也帮不上——它解决的是主库单点,不是容量。

这时候需要的是把这 20G 拆到 4 台机器上,每台 5G。Redis 集群的分片单位是槽(hash slot)。

二、直觉模型:16384 个槽,分给各主节点

key 落在哪个槽是算出来的:  slot = CRC16(key) % 16384
集群把 16384 个槽分给各个主节点:

  7000: 槽     0 - 5460
  7001: 槽  5461 - 10922
  7002: 槽 10923 - 16383

每个主节点只存"自己那几个槽"里的 key。

三条推论:

  1. key → 槽的映射是纯计算,不需要问服务器。任何客户端都能自己算,这是"客户端路由"的基础。
  2. 槽是迁移的最小单位(第 06 章扩容时搬的就是槽),所以上限 16384 决定了迁移粒度。
  3. 一个 key 永远只属于一个槽,因此也永远只在一台机器上——这是后面所有限制(多键、事务、Lua)的根源。

三、精确定义:三个必须分清的概念

概念 定义 常见误用
hash slot key 空间被切成 16384 份,编号 0~16383,CRC16(key) % 16384 决定归属 把"槽"和"节点"混为一谈——槽是逻辑单位,节点是物理承载
hash tag key 中第一个 { 与其后第一个 } 之间的子串,只有它参与 CRC16 计算 以为 {} 是命名空间或分组标记,其实它只是"强制同槽"的工具
MOVED 服务端返回的永久重定向:这个槽现在归别人了,请更新路由表 把它和 ASK 混为一谈(第 06 章区分)

hash tag 的三条细则(写 key 设计时直接用到):

四、一次完整运行:搭一个三主集群并观察路由

节点配置里必须开 cluster-enabled:

port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000

Redis 5 起 redis-cli 自带 --cluster 子命令(不再需要 Ruby 脚本):

redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 --cluster-yes

实测输出:

>>> Performing hash slots allocation on 3 nodes...
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
>>> Performing Cluster Check (using node 127.0.0.1:7000)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

最后一行是上线前的硬检查:只要有一个槽没人认领,集群就不能完整工作。

看集群整体状态:

127.0.0.1:7000> CLUSTER INFO
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:3
cluster_size:3
cluster_current_epoch:3
cluster_my_epoch:1

cluster_size:3 是主节点数(不含副本);cluster_slots_assigned 必须等于 16384。

看谁负责哪段槽:

127.0.0.1:7000> CLUSTER NODES
f155ef94f02bb9633e0fae90bbe03172e7817f9f 127.0.0.1:7000@17000 myself,master - 0 1790917272000 1 connected 0-5460
6eed45e7ac37a6cd9f86a3cd4c3b176b080cccdd 127.0.0.1:7001@17001 master - 0 1790917275371 2 connected 5461-10922
ee93b89b902f6fec25e8b16c397504fedb2740e6 127.0.0.1:7002@17002 master - 0 1790917274276 3 connected 10923-16383

每行结构:节点ID 地址:数据端口@集群总线端口 角色 主节点ID ... 连接状态 负责的槽。注意 7000@17000——集群总线端口 = 数据端口 + 10000,节点之间用它做 gossip。防火墙必须放行这两个端口,否则节点互相看不见(搭集群最常见的失败原因)。

本课的实测集群是 3 主 0 从(为看清槽路由)。生产环境每个主节点都要配至少 1 个副本,否则主节点挂掉后它那部分槽完全不可用。

算给你看:key 到槽

127.0.0.1:7000> CLUSTER KEYSLOT user:1000
1649
127.0.0.1:7000> CLUSTER KEYSLOT article:42
5605
127.0.0.1:7000> CLUSTER KEYSLOT cart:9
1156

再看 hash tag 的效果——两个业务前缀不同、但 {} 里都是 1000 的 key:

127.0.0.1:7000> CLUSTER KEYSLOT order:{1000}:items
11326
127.0.0.1:7000> CLUSTER KEYSLOT order:{1000}:addr
11326

同一个槽。这就是 hash tag 的全部意义:让"逻辑上属于同一组"的 key 落在同一台机器上,从而可以做多键操作。

五、MOVED:送错节点时 Redis 怎么回答

连到 7000,却要写一个属于 7001 的 key:

127.0.0.1:7000> SET article:42 hello
MOVED 5605 127.0.0.1:7001

服务端没有帮你转发,它只是告诉你"这个槽(5605)在 7001"。普通客户端会把 MOVED 当成错误抛给用户;支持集群的客户端(或 redis-cli -c)会自己跳过去重发:

$ redis-cli -c -p 7000 SET article:42 hello
OK
$ redis-cli -c -p 7000 GET article:42
hello

客户端路由表

集群客户端在内存里维护一张 槽 → 节点 的表,启动时用 CLUSTER SLOTS 拉全量,之后遇到 MOVED 就更新对应槽段:

flowchart TD
    A["客户端要执行 SET article:42 hello"] --> B["本地算 slot = CRC16(key) % 16384 = 5605"]
    B --> C{"路由表里有 5605 吗?"}
    C -- 有 --> D["直连该节点发命令"]
    C -- 没有 --> E["随便挑一个已知节点发"]
    D --> F{"返回 MOVED?"}
    E --> F
    F -- 否 --> G["命令完成"]
    F -- 是 --> H["按 MOVED 更新路由表<br/>(这个槽归别人了)"]
    H --> I["向新节点重发命令"]
    I --> G
    I -.->|"槽正在迁移中<br/>(第 06 章)"| J["返回 ASK:一次性跳转<br/>不更新路由表"]

图上最后那条分支是第 06 章的主题:ASK 是"临时借道",MOVED 是"永久改道",客户端处理方式不同。

六、上集群必须接受的三个能力损失

损失一:跨槽多键操作被直接拒绝

127.0.0.1:7000> MGET user:1000 article:42
CROSSSLOT Keys in request don't hash to the same slot

用 hash tag 让它们同槽就能通过:

127.0.0.1:7002> MSET order:{1000}:items a order:{1000}:addr b
OK
127.0.0.1:7002> MGET order:{1000}:items order:{1000}:addr
1) "a"
2) "b"

影响面:MGET/MSET/SUNIONSTORE/SINTER 等多键命令、MULTI 事务里的所有 key、以及 Lua 脚本里的所有 key 都必须落在同一个槽。

对本站点的影响很具体:Redis 工程课第 04 章那个分布式锁只碰 1 个 key,在集群下没问题;但 ARQ 这类一次操作碰多个 key 的组件,就必须在集群下重新设计 key(第 07 章展开)。

损失二:只有 db 0

集群不支持 SELECT,多数据库概念不存在。现有代码里若有 SELECT 1 之类的切换,迁移前必须改掉。

损失三:迁移期间的行为变化

槽在迁移中时,请求可能收到 ASK 而不是 MOVED,客户端处理不当会出错——第 06 章专门讲。

七、客户端实测:redis-py 的集群模式

import redis
from redis.cluster import RedisCluster

# 只需要给一个(或几个)种子节点,其余靠 CLUSTER SLOTS 自动发现
r = RedisCluster(host="127.0.0.1", port=7000, decode_responses=True, protocol=2)

实测输出:

== 路由表(客户端本地缓存的槽分布) ==
  0-1648 -> {'primary': ('127.0.0.1', 7000), 'replicas': []}
  1650-5460 -> {'primary': ('127.0.0.1', 7000), 'replicas': []}
  1649-1649 -> {'primary': ('127.0.0.1', 7001), 'replicas': []}
== 跨槽写入(客户端自动找节点) ==
  user:1000 = alice  (slot 1649)
  article:42 = hello  (slot 5605)
  cart:9 = x  (slot 1156)
== 跨槽多键:客户端自己拦下来 ==
  RedisClusterException: MGET - all keys must map to the same key slot
== hash tag 让两个 key 落进同一个槽 ==
  slots: 11326 11326
  mget: ['a', 'b']

三点值得注意:

  1. 路由表是客户端自己维护的,注意这里 1649 是单独一段——因为第 06 章的实验把它单独迁走了。槽段不需要连续。
  2. 跨槽写入对业务代码透明:set 不同 key 时客户端自动选节点,你感觉不到分片存在。这是集群最舒服的部分。
  3. 跨槽多键在客户端就被拦下了(RedisClusterException),不会发到服务端。也就是说这个错误在改代码时就会暴露——前提是测试覆盖了多键路径。

读写分离在集群里同样可用(read_from_replicas=True,前提是有副本)。

八、集群自带的高可用(不需要哨兵)

集群节点之间通过集群总线做 gossip,主节点失败时由它的副本顶上:

所以集群方案不需要再叠一层哨兵——这是很多人重复建设的地方。

九、误判澄清

误解 核对 结论
"16384 是最大节点数" 它是槽数,与节点数无关 官方建议主节点不超过 1000 个(gossip 开销),槽始终是 16384
"集群是高可用的升级版,顺便解决容量" 恰恰相反:集群主要解决容量与写吞吐,高可用是附带的 只想要高可用 → 主从 + 哨兵更简单
"上了集群,热点 key 就不热了" 一个 key 只属于一个槽、一台机器 热点 key 要改 key 设计或加本地缓存,分片救不了
"MOVED 是错误,说明出问题了" MOVED 是正常协议的一部分,尤其客户端冷启动时 优秀客户端会静默处理;把它当异常抛给用户是缺陷
"hash tag 用来给 key 分组" 它只影响参与 CRC16 计算的那段字符串 用错会让大量 key 挤进同一个槽,形成数据倾斜

十、失败注入清单

  1. 跨槽 MGET / Lua:故意写一条跨槽命令,确认报错形态(CROSSSLOT 或客户端异常),检查代码有没有吞掉它。
  2. 关掉一个主节点(无副本时):观察 cluster_state 是否变 fail、有多少槽变成 fail、客户端是否报错;然后给每个主补上副本再重复一次,对比结果。
  3. 堵住集群总线端口(数据端口通、总线端口不通):观察节点能否组成集群——这是验证防火墙配置最直接的一項。

十一、生产环境怎样替换

教学替身 生产替换 要点
3 主 0 从 每主至少 1 副本(常见 3 主 3 从) 无副本时主节点故障 = 那部分槽完全不可用
本机多端口 跨机器、跨机架/可用区 主与它的副本必须在不同机器上
手工 --cluster create 编排脚本 / 托管服务,并纳入变更流程 建群操作要可重复、可回滚
单 key 操作的教学示例 全量改造多键、Lua、事务 这是上集群最大的工程量,提前评估

十二、练习与验收

练习 1:给你的系统里 5 个需要多键操作的场景设计 hash tag,用 CLUSTER KEYSLOT 验证它们确实同槽,并检查有没有造成槽倾斜。

练习 2:用 redis-cli -c 连一个节点,依次访问落在不同节点的 key 观察重定向;再用 CLUSTER SLOTS 打印完整路由表,与自己算的 KEYSLOT 对照。

练习 3:列出代码里所有 MULTI / Lua / 多键命令,逐个标注"集群下能否运行",给出改造方案(加 hash tag / 拆分 / 换实现)。

验收点:不看资料,说出 16384、CRC16 取模、hash tag 三条细则、MOVED 的含义与客户端该做什么、集群的三个能力损失、以及为什么集群不需要再叠哨兵。


现在能解释什么:槽与节点的关系;key 到槽的计算与 hash tag 的作用;MOVED 语义与客户端路由表;集群下多键/事务/Lua 的限制;集群自带的 PFAIL/FAIL 与副本提升。

下一步:第 06 章做集群最核心的运维动作——在线扩容,以及迁移期间那个语义不同的 ASK。

进入 keel 阅读