2026-06-01 00:35:50 +08:00
|
|
|
|
---
|
|
|
|
|
|
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
|
2026-06-01 10:06:11 +08:00
|
|
|
|
flowchart LR
|
|
|
|
|
|
subgraph hashTagRule["Hash Tag 计算规则"]
|
|
|
|
|
|
A["rate:limit:{user123}:daily"] --> B["提取 {} 内内容: user123"]
|
|
|
|
|
|
B --> C["CRC16(user123) % 16384"]
|
|
|
|
|
|
C --> D["落点 = slot X"]
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
E["rate:limit:{user123}:hourly"] --> B
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
F["rate:limit:user456:daily"] --> G["无 {} → 取完整 Key"]
|
|
|
|
|
|
G --> H["CRC16(rate:limit:user456:daily) % 16384"]
|
|
|
|
|
|
H --> I["落点 = slot Y (通常 ≠ X)"]
|
2026-06-01 00:35:50 +08:00
|
|
|
|
|
2026-06-01 10:06:11 +08:00
|
|
|
|
style D fill:#c8e6c9
|
|
|
|
|
|
style I fill:#fff3e0
|
|
|
|
|
|
end
|
2026-06-01 00:35:50 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
| 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 <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 架构概览
|