Redis Cluster 数据分片:Hash Slot
问题
Redis Cluster 的 16384 个 Hash Slot 是怎么分配的?客户端怎么路由?为什么是 16384 而不是更多?
分析
Hash Slot 基础原理
Redis Cluster 采用分片(sharding) 来突破单机内存瓶颈。不同于简单的一致性哈希,Redis 设计了一套固定 Hash Slot 方案:
- 整个 keyspace 被划分为 16384 个固定槽位(0 ~ 16383)
- 每个 key 通过
CRC16(key) % 16384计算所属 Slot - 集群中每个节点负责一段连续或分散的 Slot 区间
- 增删节点时,只迁移受影响的 Slot,不需要全量重哈希
比如一个 3 节点集群的典型分配:
节点 A: 0 ~ 5460
节点 B: 5461 ~ 10922
节点 C: 10923 ~ 16383客户端路由机制
客户端连接集群中的任意节点,执行命令时有两种情况:
- 命中:key 的 Slot 落在当前节点,直接执行并返回
- 未命中:返回
MOVED {slot} {ip}:{port}重定向,客户端缓存该映射关系,下次直接访问目标节点
Client → 节点 A (请求 key="user:100")
CRC16("user:100") % 16384 = 12000
→ 节点 A 不负责 Slot 12000
→ 返回 MOVED 12000 192.168.1.3:6379
→ 客户端缓存 {12000 → 192.168.1.3:6379}
→ 重新请求节点 C在线扩缩容与 ASK 重定向
Cluster 的一个核心能力是在线迁移 Slot,不需要停机。流程:
- 在目标节点执行
CLUSTER SETSLOT {slot} IMPORTING {source_node_id} - 在源节点执行
CLUSTER SETSLOT {slot} MIGRATING {source_node_id} - 从源节点迭代迁移数据到目标节点(
MIGRATE命令,批量迁移 key) - 迁移完成后在任意节点执行
CLUSTER SETSLOT {slot} NODE {target_node_id}广播槽位归属
迁移过程中,客户端可能收到 ASK 重定向(区别于 MOVED):
MOVED:槽位已永久归属其他节点,客户端应更新缓存ASK:槽位正在迁移中,数据可能在源节点也可能在目标节点,客户端需要先发ASKING命令再请求目标节点,但不更新缓存(下次请求还是先问源节点)
为什么是 16384 个槽?
Redis 作者 antirez 在 GitHub 上解释过这个设计决策。16384 个槽的心跳消息用 bitmap 表示只需要 16384 bits = 2KB。如果增加到 65536 个槽,心跳消息膨胀到 8KB。而 Cluster 节点数通常不超过 1000 个,16384 个槽足够让每个节点负责 16+ 个槽,粒度已经够细。更大的槽位只会增加心跳消息的带宽消耗,没有实际收益。
跨 Slot 操作的限制与 Hash Tag
Cluster 模式下,MGET、MSET、事务、Lua 脚本等涉及多个 key 的操作,要求所有 key 落在同一个 Slot,否则报 CROSSSLOT 错误。
解决方案是 Hash Tag:用大括号 {} 包裹 key 的一部分,CRC16 只计算大括号内的内容:
user:{100}:profile → 计算 CRC16("100") % 16384
user:{100}:orders → 计算 CRC16("100") % 16384 → 同一个 Slot
user:{200}:profile → 计算 CRC16("200") % 16384 → 可能不同 Slot这样同一用户的数据就能落在同一个节点,支持原子操作。但 Hash Tag 也带来了热 key 集中的问题——如果某个用户的数据量特别大,对应的 Slot 会成为热点。
代码示例
用 Python 模拟 CRC16 取模路由
import crc16 # 需要 pip install crc16 或使用 redis-py 内置
def crc16_mod16384(key: str) -> int:
"""模拟 Redis Cluster 的 Slot 计算"""
# 检查 Hash Tag
if '{' in key and '}' in key:
start = key.index('{')
end = key.index('}', start)
if end > start + 1:
key = key[start+1:end]
hash_val = crc16.crc16xmodem(key.encode())
return hash_val % 16384
# 验证
keys = ["user:100", "user:100:profile", "product:42", "session:abc123"]
for k in keys:
slot = crc16_mod16384(k)
print(f"key={k:25s} slot={slot:5d}")
# Hash Tag 效果
keys_with_tag = ["user:{100}:profile", "user:{100}:orders", "user:{200}:profile"]
for k in keys_with_tag:
slot = crc16_mod16384(k)
print(f"key={k:30s} slot={slot:5d}")Redis 集群状态查询
# 查看集群节点信息
redis-cli -c -h 127.0.0.1 -p 7000 CLUSTER NODES
# 查看槽位分配
redis-cli -c -h 127.0.0.1 -p 7000 CLUSTER SLOTS
# 查看某个 key 的槽位和归属节点
redis-cli -c -h 127.0.0.1 -p 7000 CLUSTER KEYSLOT mykey
# 模拟迁移(将 Slot 100 从节点 A 迁移到节点 B)
redis-cli -h 127.0.0.1 -p 7001 CLUSTER SETSLOT 100 IMPORTING <node_a_id>
redis-cli -h 127.0.0.1 -p 7000 CLUSTER SETSLOT 100 MIGRATING <node_a_id>
redis-cli -h 127.0.0.1 -p 7000 CLUSTER SETSLOT 100 NODE <node_b_id>用 redis-py 连接 Cluster 并处理路由
from redis.cluster import RedisCluster
rc = RedisCluster(
startup_nodes=[{"host": "127.0.0.1", "port": "7000"}],
decode_responses=True
)
# 正常读写,客户端自动处理 MOVED/ASK
rc.set("foo", "bar")
print(rc.get("foo")) # "bar"
# MGET 跨 slot 会报错,需要用 Hash Tag
rc.mset({"{user:100}:name": "Alice", "{user:100}:age": "30"}) # 同一 slot
print(rc.mget("{user:100}:name", "{user:100}:age")) # ['Alice', '30']
# 查看节点映射
nodes = rc.get_nodes()
for node_id, node in nodes.items():
slots = node.slots
print(f"Node {node_id}: slots {min(slots)}-{max(slots)}")总结
Redis Cluster 的 Hash Slot 分片设计在一致性和消息开销之间做了精巧的权衡。16384 个固定槽位配合 bitmap 心跳,让 1000 节点规模的集群也能高效通信。客户端路由模型(MOVED/ASK 重定向)虽然简单,但配合 Hash Tag 和客户端缓存,已经能满足绝大多数分布式缓存场景的需求。
理解 Hash Slot 机制是深入 Redis Cluster 的第一步——后续的 Slot 迁移、resharding、reshard 限流、读写分离配置,都建立在这个分片模型之上。排查线上问题时,CLUSTER KEYSLOT 和 CLUSTER SLOTS 是最常用的诊断命令,建议记熟。