--- tags: [redis, cluster, hash-slot, routing-key, hash-tag] create time: 2026-06-01 10:30 --- # Redis Cluster 路由键与 Hash Tag ## 概述 当你把 Redis 部署成集群模式后,数据不再存在于一台机器上——而是被拆分到多个节点。这个拆分的单位叫做 **hash slot(哈希槽)**。理解路由机制是正确使用 Redis Cluster 的前提,否则你可能会遇到 "Key 跨 slot、Lua 脚本执行失败" 这种让人头疼的问题。 ## 一、Hash Slot 基础 Redis Cluster 固定分配 **16384 个 slot**(编号 0 ~ 16383)。每个 Key 在存入前都会被计算一个 slot 编号,决定它落在哪个节点上: ```go slot := crc16(key) % 16384 ``` **这意味着:** - 同一个 Key 永远落在同一个 slot - 不同 Key 可能碰撞到同一个 slot(取决于 CRC16 结果) - 16384 个 slot 均匀分布在所有主节点上 > [!question]- 💡 思考:为什么是 16384? > > 这个数字不是随意选的。早期设计者考虑过:如果只有 1024 个 slot,节点多了单个节点的负载会不均衡;如果 100 万+,slot 迁移和拓扑管理的开销又太大。16384 是一个经验值——足够细粒度地分布,又不会让管理成本爆炸。 ## 二、Hash Tag:强制锁定同一 Slot 这是本节最重要的实战概念。**`{}` 包裹的部分叫 Hash Tag**,它会告诉 Redis:"不管后面的字符串差异多大,整个 Key 都走 `{}` 内的内容来计算 slot"。 ### 2.1 规则 ```mermaid flowchart LR subgraph hashTagRule["Hash Tag 计算规则"] A["rate:limit:{user123}:daily"] --> B["提取 {} 内内容: user123"] B --> C["CRC16(user123) % 16384"] C --> D["落点 = slot X"] E["rate:limit:{user123}:hourly"] --> B F["rate:limit:user456:daily"] --> G["无 {} → 取完整 Key"] G --> H["CRC16(rate:limit:user456:daily) % 16384"] H --> I["落点 = slot Y (通常 ≠ X)"] style D fill:#c8e6c9 style I fill:#fff3e0 end ``` | Key 示例 | 实际参与计算的字符串 | 落点 | |----------|-------------------|------| | `rate:limit:user123:daily` | `rate:limit:user123:daily`(完整 Key) | slot Y | | `rate:limit:{user123}:daily` | `user123`({} 内部分) | slot X | | `rate:limit:{user123}:hourly` | `user123`(相同!落到同一 slot) | slot X ✓ | **核心结论:** 使用相同的 Hash Tag 的所有 Key,一定路由到同一个 slot。 ### 2.2 为什么需要它? Lua 脚本中涉及多个 Key 时,这些 Key **必须在同一个 slot**。例如令牌桶脚本同时操作 `tokensKey` 和写入 `EXPIRE`: ```lua local tokensKey = KEYS[1] -- HMSET + EXPIRE 都需要操作同一 slot redis.call('HMSET', tokensKey, 'tokens', remaining, 'last_refill', now) redis.call('EXPIRE', tokensKey, math.ceil(capacity / rate) * 2) ``` 如果你构造的 Key 没有用 `{}` 包裹,在高并发下不同用户的数据可能散落在不同 slot,当 Lua 脚本需要跨 slot 多 Key 操作时就会报错: ``` CROSSKEYS Lua script tried to access keys in different slots ``` ### 2.3 正确实践 ```go // ❌ 错误:不同维度可能分到不同 slot key := fmt.Sprintf("rate:limit:%s:daily", userId) // user123:daily 算 slot key := fmt.Sprintf("rate:limit:%s:hourly", userId) // user123:hourly 算 slot → 不同 slot! // ✅ 正确:用 {} 包裹业务标识,保证同用户所有 Key 同 slot key := fmt.Sprintf("rate:limit:{%s}:daily", userId) // {user123} 算 slot key := fmt.Sprintf("rate:limit:{%s}:hourly", userId) // {user123} 算 slot → 同一 slot ✓ ``` > [!tip]- Hash Tag 的最佳选择 > > `{}` 内的字符串应该尽量简短且区分度高。常见做法: > - 用户 ID:`{user123}` > - IP 地址段:`{192.168.1}` > - API Key:`{app_key_abc}` > - **避免用复杂字段**(如 UUID 全量),这会增加 CRC16 计算的无意义熵但不提升安全性。 ## 三、常见问题排查 ### 问题:Cluster 下限流失效/报错 ``` MISDIRCTED or CROSSKEYS error ``` **原因:** Lua 脚本中的多个 Key 分属不同 slot。 **排查步骤:** 1. 打印 Key 格式,确认是否包含 Hash Tag 2. 检查是否在同一脚本中混用了不同前缀的 Key 3. 确认 `{}` 括号内的标识符是一致的 **解决:** 统一使用 `{业务ID}` 格式构造 Key。 ## 四、测试 Hash Tag 效果 本地可以直接验证两个 Key 是否落到了同一 slot: ```bash # CRC16 计算工具(需安装 crc16 或自己写脚本) # 或者通过 Redis Cluster 节点直接查询 CLUSTER GETKEYSINSLOT # 快速验证思路: # 在开发环境启动一个 3 节点的 Cluster,观察 key 分布 redis-cli -p 7000 CLUSTER NODES ``` > [!warning]- Hash Tag 不是银弹 > > 所有用了相同 Hash Tag 的 Key 都会落在同一个 slot,意味着 **该 slot 所在节点的负载会偏高**。不要把所有 Key 都用 `{all}` 或 `{1}` ——那样等于没做分布式。Hash Tag 只应该包裹真正需要关联操作的标识符(如用户 ID)。 ## 关联笔记 - [[分布式限流]] — Redis Cluster 限流的 Key 设计与 Hash Tag 应用 - [[Lua脚本]] — Lua 脚本多 Key 操作的 slot 约束 - [[02-服务治理/02-数据库中间件]] — Redis Cluster 架构概览