Skip to content

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

客户端路由机制

客户端连接集群中的任意节点,执行命令时有两种情况:

  1. 命中:key 的 Slot 落在当前节点,直接执行并返回
  2. 未命中:返回 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,不需要停机。流程:

  1. 在目标节点执行 CLUSTER SETSLOT {slot} IMPORTING {source_node_id}
  2. 在源节点执行 CLUSTER SETSLOT {slot} MIGRATING {source_node_id}
  3. 从源节点迭代迁移数据到目标节点(MIGRATE 命令,批量迁移 key)
  4. 迁移完成后在任意节点执行 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 取模路由

python
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 集群状态查询

bash
# 查看集群节点信息
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 并处理路由

python
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 KEYSLOTCLUSTER SLOTS 是最常用的诊断命令,建议记熟。

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。