5.2 KiB
tags, create time
| tags | 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 编号,决定它落在哪个节点上:
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。
排查步骤:
- 打印 Key 格式,确认是否包含 Hash Tag
- 检查是否在同一脚本中混用了不同前缀的 Key
- 确认
{}括号内的标识符是一致的
解决: 统一使用 {业务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)。
关联笔记
- 分布式限流 — Redis Cluster 限流的 Key 设计与 Hash Tag 应用
- Lua脚本 — Lua 脚本多 Key 操作的 slot 约束
- 02-服务治理/02-数据库中间件 — Redis Cluster 架构概览