DEV Community

Truman
Truman

Posted on

深度解析:利用 Redis Hash Tag 解决 Redis Cluster 下批量读取的延迟抖动

在 Redis Cluster 中,keyspace 会被切分到 16384 个 hash slot,每个 slot 在任意时刻只由一个节点负责;集群通过 master/replica 复制实现高可用。(Redis)
slot 的计算基于 CRC16,并对 16384 取模。(Redis)
同时,Cluster 设计目标之一就是 无代理(no proxies)、不做服务端 merge 操作,客户端通过重定向(如 MOVED / ASK)连接到正确节点执行。(Redis)

这套设计带来一个典型现象:单机里很快的批量读(MGET / pipeline)到 Cluster 上,P99 抖动明显变大,甚至超时。


一、问题根因:跨 slot 的 fan-out 放大尾延迟

Redis Cluster 对“复杂多 key 操作”有明确边界:只有当涉及的 keys 都 hash 到同一 slot 时才支持;否则 multi-key 能力会不可用。(Redis)

因此,业务上一批相关 key 如果分散到多个 slot:

  • MGET/MSET 这类 variadic multi-key 命令会受限(常见表现是 CROSSSLOT)。
  • 即便不使用 MGET,而是改成多次 GET 或 pipeline,本质上仍会变成 多节点请求 + 合并结果(scatter-gather),整体耗时由最慢节点拖累。

二、核心技术:Hash Tag 的作用是把 M 个节点收敛到 1 个节点

Cluster 通过 hash tag 提供“数据亲和性”:强制一组相关 key 落在同一 slot,从而允许 multi-key 操作并显著降低跨节点 fan-out。(Redis)

Hash Tag 规则(严格按 cluster-spec)

当且仅当满足以下条件时启用 {...}:(Redis)

  1. key 包含 {
  2. { 右侧存在 }
  3. 第一对 { 与其右侧第一个 } 之间至少有 1 个字符

满足时:只对这段子串做 CRC16 以计算 slot。(Redis)

{} 不生效:例如 foo{}{bar} 会对整串 key 正常哈希,而不是对空串哈希。(Redis)


三、收益来自哪里:减少跨节点 fan-out,而不是“Redis 执行更快”

示例(收藏场景):

  • 优化前:favorites:123:news001favorites:123:news010
    key 全名不同 → slot 大概率不同 → 原生 MGET 会因跨 slot 报错,而客户端模拟的批量操作会因抖动导致延迟剧增。

  • 优化后:favorites:{...}:news001favorites:{...}:news010
    tag 相同 → 同一 slot → 同一节点 → 从“多节点并发 + 合并”收敛为“单节点一次返回”。


四、相关问题(工程边界与常见误解)

1){...} 内容相同会不会“值乱/冲突”?

不会。
Hash Tag 只影响 落在哪个 slot/节点,不会影响 key 的唯一性;是否覆盖只取决于 完整 key 字符串是否相同

真正需要防的是:不同业务/不同服务没做命名空间隔离导致完整 key 撞名,而不是 tag 重复。

2)Tag 是否只能放一个 userId

从正确性角度:只放 userId 完全可行。(Redis)
从负载与“亲和边界”角度:tag 的内容应表达“哪些 key 必须放在一起”。

如果多个业务域都用 {userId},同一用户的跨业务访问会更倾向集中到同一 slot(不乱值,但可能叠加热点)。更稳的做法是使用 复合 tag 限定亲和边界,例如:

  • favorites:{fav:123}:news001
  • orders:{ord:123}:order8899

这样仍可保证“服务内/业务内” multi-key 能力,同时减少“跨业务不必要绑死在同一 slot”的概率。

3)即便用了 Hash Tag,混用不同 Tag 仍会失败

例如一次 MGET 混合 {u1}{u2},本质上仍是跨 slot,multi-key 约束依然不满足。Redis Cluster 的 multi-key 能力边界是“同 slot”,hash tag 只是让“同 slot”变得可控。(Redis)


五、Key 命名规范与 Tag 设计准则(落地规范)

下面这段可以直接作为团队规范的“统一模板”。

1)命名空间(必须)

保证不同服务/不同模块不会撞 key。

推荐前缀结构:

  • <env>:<service>:<module>:...

示例:

  • prod:news:favorites:{fav:123}:news:001
  • test:uc:session:{sess:uid123}:token

2)Tag 放置与内容(核心)

推荐结构:

  • <prefix>:{<affinity>}:<suffix>

其中 <affinity> 用于定义“亲和边界”,建议采用 业务域 + 主维度

  • {fav:<userId>}:收藏业务按用户聚合
  • {ord:<orderId>}:订单业务按订单聚合
  • {cart:<userId>}:购物车按用户聚合

这样做的效果是:

  • 同业务内可 multi-key(同 slot)
  • 跨业务不会因为同一个 userId 产生不必要的 slot 绑定

注意:Cluster 只会使用第一对有效 {...} 参与哈希;空 {} 会被当作无效,整串 key 哈希。(Redis)

3)字段与层级(可读性 + 可扩展)

建议用固定分隔符(常用 :),并把“对象类型/字段”显式写出来:

  • prod:news:favorites:{fav:123}:news:001(对象:news,id:001)
  • prod:news:favorites:{fav:123}:meta(聚合元信息)
  • prod:uc:profile:{uc:123}:base(用户基础资料)

4)版本号(建议)

当 value 结构可能升级时,加版本避免回滚/灰度混乱:

  • prod:news:favorites:v1:{fav:123}:news:001
  • prod:news:favorites:v2:{fav:123}:news:001

5)热点与倾斜(必须评估)

Hash Tag 会把一组 key 固定到同一 slot;tag 粒度过粗会造成倾斜。Cluster-spec 也明确 hash tag 的用途是为了 multi-key,同样需要避免把大量不相关 key 强行塞到一个 slot。(Redis)

禁止示例:

  • {common}{config} 这类全局 tag
  • 用一个 tag 聚合“全站数据”

Top comments (0)