登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  Redis

Redis Cluster 批量读取报 CROSSSLOT:Hash Tag、MGET 与迁移期检查

来源:17golang原创

时间:2026-07-19 11:23:08 259浏览 收藏

下午补了一次商品详情页的聚合读取,单机环境一直正常,上到 Redis Cluster 后却在高峰前先冒出一串 CROSSSLOT Keys in request don't hash to the same slot。代码里只是把价格、库存和活动状态合成一次 MGET,真正出问题的是 Key 名字:它们看着都属于同一个 SKU,计算槽位时却没有共同的哈希片段。

实践要点

  • 先用 CLUSTER KEYSLOT 对比实际槽位,不要只凭 Key 前缀猜测。
  • 需要原子或批量访问的 Key,使用同一个业务维度的 Hash Tag,例如 {sku:8472}
  • Hash Tag 只能服务有明确聚合边界的少量 Key,不能把所有业务数据塞进一个槽位。
  • 扩缩容期间,客户端仍要正确处理 MOVEDASK,并把多 Key 调用纳入回归检查。

现场不是 MGET 失效,而是两个 Key 落在了不同槽位

当时的商品 Key 很朴素:

product:8472:price
product:8472:stock
promo:8472:state

接口希望一次取回前两个值,Go 侧大致是这样:

keys := []string{
    "product:8472:price",
    "product:8472:stock",
}

values, err := rdb.MGet(ctx, keys...).Result()
if err != nil {
    return nil, err
}

单实例没有槽位边界,命令自然能完成。Redis Cluster 则会先判断多 Key 操作涉及的槽位;只要不一致,服务端直接拒绝,而不是把请求拆开转发。这里别急着把 MGET 改成多个 GET。那样能让报错暂时消失,却会把一次聚合读取变成多次网络往返,也会让页面拿到混合时刻的数据。

先用 CLUSTER KEYSLOT 看清两个 Key 为什么分家

排查不需要猜 CRC16。连到任意集群节点,分别计算槽位即可:

redis-cli -c CLUSTER KEYSLOT product:8472:price
redis-cli -c CLUSTER KEYSLOT product:8472:stock
redis-cli -c CLUSTER KEYSLOT promo:8472:state

如果三个返回值不同,问题已经坐实:普通 Key 会以整个字符串参与槽位计算,前缀相同并不代表同槽。日志里还应保留业务 Key 的脱敏样本、命令名和槽位数字;只记录一条 CROSSSLOT 文本,后面很难判断是哪组字段设计错了。

Redis Cluster 中 price 与 stock Key 分属不同槽位,MGET 被 CROSSSLOT 拒绝的对照图

别把错误归到客户端连接方式

-c 的 redis-cli 或支持 Cluster 的 SDK,能跟随节点返回的重定向;这解决的是“该去哪个节点”。CROSSSLOT 是另一层问题:服务端无法在多个槽位上完成这一条多 Key 命令。若 SDK 切换为 Cluster 模式后才暴露错误,通常只是它终于不再把请求发给单机代理,而不是 SDK 新引入了故障。

把同一商品的 Key 收进一个 Hash Tag,再补迁移期检查

Hash Tag 的规则很直接:Key 中第一个有效 {...} 里的内容用于计算槽位。把“同一商品”作为聚合边界,可以改成下面这样:

product:{sku:8472}:price
product:{sku:8472}:stock
promo:{sku:8472}:state

现在三个 Key 都会按 sku:8472 计算槽位,价格与库存的 MGET、缓存更新时的事务组合都具备了同槽前提。改名不是只改一处常量:写入路径、失效通知、批处理任务和历史 Key 回收都要一起核对,否则新旧命名并存时,页面仍可能读到旧值。

使用 sku Hash Tag 将商品价格库存归入同一 Redis 槽位,并检查 ASK 与 MOVED 的流程图

Hash Tag 的粒度要小,避免制造热点槽位

{tenant:42} 适合该租户内少量必须一起访问的配置;把所有租户都写成 {tenant},等于主动把大量数据压到同一个槽位。更常见的商品场景里,用 {sku:8472} 比用店铺或全站维度更稳。判断方式很朴素:这组 Key 是否真的需要一起读、一起改,数量是否可以被单个业务实体约束。

迁移中的 ASK 与 MOVED 不能靠重试掩盖

扩容、缩容或重新分片时,槽位会在节点间移动。正常的 Cluster 客户端应根据 MOVED 刷新路由;遇到 ASK 时,按协议把本次请求送到迁入节点。应用侧不该把这两类响应粗暴地当成“Redis 超时”做无差别重试,更不要在这个窗口把同槽要求改回多次单 Key 读取。

改完 Key 以后,给聚合读取补一组能抓住回归的检查

这类问题最容易在新增字段时回来:有人给商品详情再加一个 product:8472:coupon,忘了保留 Hash Tag,测试数据少时不一定立刻覆盖。把 Key 规则和命令检查留在代码旁边,成本比线上追请求低得多。

func productKeys(sku string) (string, string, string) {
    tag := "{sku:" + sku + "}"
    return "product:" + tag + ":price",
        "product:" + tag + ":stock",
        "promo:" + tag + ":state"
}

func loadProduct(ctx context.Context, rdb redis.Cmdable, sku string) ([]interface{}, error) {
    priceKey, stockKey, promoKey := productKeys(sku)
    return rdb.MGet(ctx, priceKey, stockKey, promoKey).Result()
}

测试环境可以对每个要合并读取的 Key 调用 CLUSTER KEYSLOT,断言结果相等。生产前再用真实 Cluster 做一次商品详情压测,至少观察错误计数、P95 延迟和节点重定向次数。后两项突然上涨时,先看集群是否正在迁移,再检查是否混入了未带 Hash Tag 的新字段。

观察到的现象先查哪里处理方向
MGET 立即返回 CROSSSLOT两条 Key 的 KEYSLOT统一业务实体的 Hash Tag
偶发 MOVED客户端路由与节点拓扑确认 SDK 的 Cluster 支持与刷新能力
扩容时出现 ASK槽位迁移状态保留协议处理,完成迁移后复查
单槽负载过高Hash Tag 粒度与 Key 数量缩小聚合边界,拆分业务实体

几个常见问题

所有 Redis 命令都要求同槽吗?

不是。单 Key 命令由该 Key 所在节点处理;涉及多个 Key 的命令才需要特别关注槽位边界。先看命令参数里是否同时带了多个业务 Key。

能不能在应用里拆成多个 GET?

可以作为不需要一致快照的降级方案,但它会增加请求次数,也可能读到不同时间点的数据。对必须聚合的数据,更适合修正 Key 模型。

Hash Tag 会影响普通 GET 吗?

不会改变普通 GET 的语义,只改变该 Key 计算槽位时使用的片段。真正需要警惕的是 Tag 过粗导致大量 Key 聚到同一槽。

KEYSLOT 一样就说明业务设计正确吗?

它只证明命令可以在同一槽执行。是否应该同槽,还要看数据规模、访问热度和是否真的存在一起访问或原子更新的需求。

把槽位检查变成 Key 设计的一部分

这次报错的修复并不复杂:先用 CLUSTER KEYSLOT 拿到证据,再用细粒度 Hash Tag 把确实要一起访问的 Key 放进同一槽位,最后让迁移期的客户端处理保持可观测。比起给 CROSSSLOT 加一次临时重试,这套检查更能防住下一次新增字段时的回归。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>