bigywater.tech
常见报错 · 54

Redis CROSSSLOT Keys in request don't hash to the same slot:同节点不同槽位也报,19 个多 key 命令全部受限,固定 hash tag 让 3000 个 key 全进一个节点

CROSSSLOT 是 Redis Cluster 上多 key 命令的常见拦路虎。这篇用 Redis 8.10.2 的 3 主节点集群在本机复现:同一个节点上的 key 为什么也不行、19 个命令的清单、hash tag 的规则和容易写错的情形、事务与 Lua 里什么时候报,以及 hash tag 的代价。

19 / 19
不同槽位上的多 key 命令全部报 CROSSSLOT 的个数
[0, 0, 3000]
3000 个 {order}:i 在三个节点上的 key 数:全在一个节点
0.22 ms
同 tag 的 200 个 key 一次 MGET 的耗时,对比逐个 GET 6.75 ms

同节点也不行

实测 · 本机执行输出
  user:3     槽位  2648  所在节点 8210
  user:1     槽位 10778  所在节点 8211
  user:0     槽位 14907  所在节点 8212
  user:7     槽位  2780  所在节点 8210
  > MGET user:3 user:1
      (error) CROSSSLOT Keys in request don't hash to the same slot
  > MGET user:3 user:3
      1) "v-user:3"
2) "v-user:3"
  对照:同一个节点上、但槽位不同的两个 key 也不行(判断标准是槽位,不是节点):
  > MGET user:3 user:7
      (error) CROSSSLOT Keys in request don't hash to the same slot

判断标准是槽位而不是节点。

命令清单

实测 · 本机执行输出
  MGET         -> CROSSSLOT
  MSET         -> CROSSSLOT
  DEL          -> CROSSSLOT
  UNLINK       -> CROSSSLOT
  EXISTS       -> CROSSSLOT
  TOUCH        -> CROSSSLOT
  SUNION       -> CROSSSLOT
  SINTER       -> CROSSSLOT
  SUNIONSTORE  -> CROSSSLOT
  RENAME       -> CROSSSLOT
  COPY         -> CROSSSLOT
  LMOVE        -> CROSSSLOT
  SMOVE        -> CROSSSLOT
  BITOP        -> CROSSSLOT
  PFMERGE      -> CROSSSLOT
  ZUNIONSTORE  -> CROSSSLOT
  SORT         -> CROSSSLOT
  WATCH        -> CROSSSLOT
  BLPOP        -> CROSSSLOT

MGET、MSET、DEL、EXISTS、RENAME、LMOVE、ZUNIONSTORE、WATCH 等多 key 命令都受限。

hash tag

实测 · 本机执行输出
  CLUSTER KEYSLOT user:1:name      -> 12440
  CLUSTER KEYSLOT user:1:age       -> 13180
  CLUSTER KEYSLOT {user:1}:name    -> 10778
  CLUSTER KEYSLOT {user:1}:age     -> 10778
  CLUSTER KEYSLOT {user:2}:name    -> 6777
  CLUSTER KEYSLOT order{1}         -> 9842
  CLUSTER KEYSLOT order{2}         -> 5649
  CLUSTER KEYSLOT {}user:1         -> 8914
  CLUSTER KEYSLOT a{}b             -> 13694
  CLUSTER KEYSLOT {{x}}            -> 11068
  CLUSTER KEYSLOT {x}{y}           -> 16287
  CLUSTER KEYSLOT foo{bar}{zap}    -> 5061
  CLUSTER KEYSLOT foo{}{bar}       -> 8363
  同槽位的一组 key 可以 MGET / MSET / 事务:
  (槽位 10778 在节点 8211 上,命令发给它)
  > MSET {user:1}:name alice {user:1}:age 30
      "OK"
  > MGET {user:1}:name {user:1}:age
      1) "alice"
2) "30"

只哈希花括号里的内容;空花括号时整个 key 哈希。

事务与 Lua

实测 · 本机执行输出
  > MULTI
      "OK"
  > SET user:3 1
      "QUEUED"
  > SET user:7 2
      "QUEUED"
  > EXEC
      (error) CROSSSLOT Keys in request don't hash to the same slot
  > EVAL return redis.call('MGET', KEYS[1], KEYS[2]) 2 user:3 user:1
      (error) CROSSSLOT Keys in request don't hash to the same slot
  > EVAL return redis.call('MGET', KEYS[1], KEYS[2]) 2 user:3 user:7
      (error) CROSSSLOT Keys in request don't hash to the same slot
  > EVAL return redis.call('GET', ARGV[1]) 0 user:1
      (error) ERR Script attempted to access a non local key in a cluster node script: 2b607d3eeffe20cb8536f3e513a6fe42580a3d6e, on @user_script:1.
  > EVAL return redis.call('MGET', KEYS[1], KEYS[2]) 2 {user:1}:name {user:1}:age
      1) "alice"
2) "30"

MULTI 到 EXEC 才报;Lua 的 KEYS 要同槽位,脚本里偷用别的 key 是另一种错误。

代价

实测 · 本机执行输出
  3000 个 {order}:i :各节点的 key 数 = [0, 0, 3000]  槽位 = 16025
  3000 个 plain:i  :各节点的 key 数 = [990, 1010, 1000]
  读 200 个 key(本机回环,7 次取中位数):逐个 GET 6.75 ms;同 tag 一次 MGET 0.22 ms;按槽位分组 MGET(200 个槽位 = 200 次请求)6.75 ms

同 tag 的 key 都在一个节点,热点与容量要自己负责。

分步演示

跨槽位
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MySQL 26.10.0 与 Redis 8.10.2(Homebrew,每个实验自己起实例,端口 9150 以上,只监听 127.0.0.1,不碰本机已有的实例)/ Python 3.15.0 + PyMySQL(按各篇注明用到哪些及版本);报错文本都是真实复现时原样捕获的;计时用 time.perf_counter,数字每次运行会有小幅抖动。本篇只用 Redis,Redis 8.10.2,3 个主节点(无从库)的集群,端口 8210–8212(避开 8181);实验结束后全部停止;读 key 的耗时是本机回环,7 次取中位数,真实网络里逐个请求的差距会更大
    • 没有验证:带从库的集群与 READONLY 读写分离下的 CROSSSLOT;Redis Cluster 之外的代理方案(Twemproxy、Envoy、Redis 企业版)对跨槽位命令的处理;Pipeline / 集群客户端(Lettuce、Jedis、redis-py cluster)自动按槽位拆分命令的具体行为;重新分片(slot 迁移)期间的 TRYAGAIN;云厂商托管集群的限制
    京ICP备2026067502号-1京公网安备11010502063487号