2169 字
约 7 分钟
16
S12-A22 · Redis 集群的槽位:16384 个格子怎么分

一次扩容,Redis 集群从 3 主扩到 6 主,运维按"平均分"迁移了槽位。上线后 MGET 批量取 20 个 key 全部失败,报 CROSSSLOT Keys in request don't hash to the same slot——因为批量操作要求"所有 key 在同一个槽",而扩容把它们的槽分散了。

Redis 集群的核心是"16384 个槽 + CRC16 取模"。而大部分"集群问题"不是"槽怎么分",这个"设计"而不是"运维"的定位很重要:槽分配是运维能做的,而"哪些 key 必须同槽"只能由业务决定——于是"扩容后报 CROSSSLOT"本质上是"上线前没想到这类操作"。
而是"你的 key 有没有被设计成"能落在同一个槽""。


1. 三个基本事实

事实 含义
16384 个槽 集群的"分片单位"
CRC16(key) % 16384 槽的归属(不看库号)
槽 → 节点 由集群配置决定(可迁移)

第二项解释了"为什么集群模式下 SELECT 不可用":槽是唯一的分片维度,库号不在其中。第二项的"另一个后果"是"不同库的同名 key 会撞":db0:user:42 与 db1:user:42 若哈希相同就落到同一槽——而集群里它们"没有库号区分",于是"本该隔离的数据互相覆盖"。

2. "CROSSSLOT" 的三种触发

触发 例子
批量取多个无关 key MGET a b c
多 key 命令(事务/Lua) MULTI 里的多个 key
跨 key 的集合操作 SINTERSTORE a b

第三项(跨 key 的集合操作)最"隐":它在代码里是一行 SINTERSTORE,而在集群里要求"所有输入 key 同槽 + 输出 key 同槽"——于是"一不小心就触发"。
修法只有一条:让相关 key 落在同一个槽,而那靠"hash tag"。

// hash tag:只对 {} 内的部分做哈希
{user:42}:profile   → 槽 = CRC16("user:42") % 16384
{user:42}:orders    → 同一个槽
user:42:profile     → 槽 = CRC16("user:42:profile")(不同槽)
// 规则:只有第一个 {} 生效;空 {} 或没有 {} 等于没写

3. "hash tag"的三种用法与风险

用法 效果
按业务实体分组({user:42}:*) 支持批量/事务
按租户分组({tenant}:*) 支持跨 key 操作,但倾斜
过度分组({all}:*) 全部落一个槽(失去分片)

第三项是"最常见的误用":为了让"所有批量操作都能跑",把所有 key 都加了同一个 tag第三项的"识别"很容易:redis-cli --cluster check 会显示各节点的"槽数与 key 数"——若某个节点占绝大多数 key,就是"过度分组"。
——于是"集群"变成了"单节点"。

第二项的"缓解"是"大租户单独拆":{tenant:big}:user:{uid} —— 于是"租户内"仍可批量,"租户间"分散。而它要求"业务上知道谁是大租户"。
第二项的风险在"热点":大租户的 key 全在一个槽,于是"一个节点扛一个大客户"。

// 好的 key 设计(按"必须一起操作的边界"分组)
{user:42}:profile, {user:42}:settings   // 需要一起读 → 同 tag
order:1001, order:1002                   // 独立 → 不加 tag(分散到不同槽)
// 判据:这两个 key "会不会出现在同一个批量/事务/脚本里"

4. 槽迁移的三种影响

影响 说明
迁移期间 MOVED 重定向 客户端要跟随
批量操作失败 跨槽(见上)
热点槽迁移慢 大 key 拖慢迁移

第三项很实际:**一个含 100 万个元素的 key,迁移会"卡住整个槽"**第三项的"量化"是"单 key 迁移时间 ≈ 元素数 × 每元素成本":100 万元的 List,迁移可能要几十秒到几分钟——而"迁移期间对这个槽的写"会被阻塞。
——于是"扩容窗口"被"大 key"决定。

所以"扩容前的检查"应当包含"扫大 key":redis-cli --bigkeys 或 MEMORY USAGE。

"同槽验证"可以在开发期就做,不必等线上报错:

// 验证一组 key 是否同槽(用 CLUSTER KEYSLOT)
redis-cli -c CLUSTER KEYSLOT "{user:42}:profile"
redis-cli -c CLUSTER KEYSLOT "{user:42}:orders"
// 两者相同 → 可以放进同一个 MGET/MULTI/Lua
// 反例:cluster keyslot "user:42:profile" 与 "user:42:orders" → 通常不同

5. 客户端要处理的三件事

事项 说明
MOVED 槽已迁到别的节点(更新本地映射)
ASK 槽正在迁移(这次去新节点,不改映射)
重试 上面的两种都要"跟随重试一次"

第二项的"错误后果"很具体:把 ASK 当 MOVED 会让客户端"过早"更新映射,于是"还没迁完的 key"被发到新节点,得到 MOVED 再被弹回——请求在两个节点之间来回。
第二项容易被实现错:把 ASK 当成 MOVED(更新映射)会导致"迁移期间的请求乱跳"。

// 正确的处理顺序
1 收到 MOVED:更新槽映射 → 重试(一次)
2 收到 ASK:不更新映射 → 对"这一次"请求发到目标节点
3 连续失败:退避重试(限次数),不要无限循环
// 反例:把 ASK 也当成"更新映射" → 迁移期间命中率骤降

"批量操作"的拆法有两种,选择取决于"是否必须一起成功":

// 两种拆法(同一个 MGET 20 个 key)
1 客户端分组:按槽分成 k 组,各组一次 MGET,结果合并(无原子性,但快)
2 加 hash tag:让 20 个 key 同槽(有原子性,但受"分组粒度"限制)
// 判据:这 20 个 key 是否"必须同时读到同一时刻的值"

6. 排错:五种"集群问题"

现象 成因 处理方向
CROSSSLOT 批量 key 不同槽 加 hash tag 或拆分
集群变单点 全部 key 同一 tag 重新设计 key
某节点 CPU 高 热点槽/大租户 拆 tag 或拆租户
扩容很慢 大 key 先拆大 key
迁移期错误率上升 ASK 处理错 修客户端

第二项最讽刺:"为了解决跨槽错误"而加的 tag,把集群变成了单点第二项的"检测"是"看"单节点 key 数占比"":超过 50% 就能判断"分片失效"——而"整体 QPS 能跑"会掩盖它,直到"单节点内存打满"。
——于是"能跑"但"没有分片收益"。

最后一个"容量规划"视角:槽数固定(16384),节点数可变——于是"节点越多,每节点槽越少",而"槽不是瓶颈,内存与 QPS 才是"。

// 三种规模的建议
< 30GB:3 主 3 从(每主约 16384/3 槽)
30–100GB:6–9 主(每主 5–10GB)
> 100GB:按"业务域"拆集群,而不是"在一个集群里加节点"
// 判据:单节点内存与连接数,而不是"槽的均衡"

7. 边界:什么情况不必关心槽

场景 为什么 做法
单节点(非集群) 无槽概念 不管
只用单 key 命令 无跨槽需求 不管
代理模式(如 Proxy) 代理处理槽 看代理配置
数据量小(< 1GB) 分片收益小 可用单节点/主从

最后一条与"客户端"有关的边界:用"集群感知"的客户端(Lettuce/Jedis Cluster)而不是"单机客户端 + 代理"——前者会"跟随 MOVED",后者依赖代理实现,而"代理对多 key 命令的支持"各不相同。
判断口径一句话:"你的业务里有没有"必须一起操作的多个 key"。有(批量取、事务、Lua),就必须设计 hash tag 让它们同槽;没有(都是单 key 操作),槽的事交给默认散列。


8. 自测

  1. 三个基本事实里,哪一条解释了"集群模式不能用 SELECT"?
  2. CROSSSLOT 的三种触发里,哪一种最难改?为什么"多 key 命令"要求同槽?
  3. hash tag 只对哪部分生效?"过度分组"会造成什么后果?
  4. 为什么"大 key"会拖慢扩容?扩容前该做什么检查?
  5. MOVED 与 ASK 的处理差别是什么?把 ASK 当 MOVED 会怎样?
  6. 判断"是否需要设计 hash tag"的那个问题是什么?
S12-A22 · Redis 集群的槽位:16384 个格子怎么分
https://www.clxhxhhr.top/posts/6609/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。