Files
cs-note/hzh/REDIS/路由键与Hash Tag.md
T

5.2 KiB
Raw Blame History

tags, create time
tags create time
redis
cluster
hash-slot
routing-key
hash-tag
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 编号,决定它落在哪个节点上:

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 规则

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:

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 正确实践

// ❌ 错误:不同维度可能分到不同 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:

# CRC16 计算工具(需安装 crc16 或自己写脚本)
# 或者通过 Redis Cluster 节点直接查询
CLUSTER GETKEYSINSLOT <slot> <count>

# 快速验证思路:
# 在开发环境启动一个 3 节点的 Cluster,观察 key 分布
redis-cli -p 7000 CLUSTER NODES

[!warning]- Hash Tag 不是银弹

所有用了相同 Hash Tag 的 Key 都会落在同一个 slot,意味着 该 slot 所在节点的负载会偏高。不要把所有 Key 都用 {all} 或 {1} ——那样等于没做分布式。Hash Tag 只应该包裹真正需要关联操作的标识符(如用户 ID)。

关联笔记