bigywater.tech
Redis · 73

Redis Cluster 的 CROSSSLOT

CROSSSLOT 是从单机迁到 Redis Cluster 时最常见的报错之一。很多人以为它的意思是「这两个 key 不在同一个节点」,其实不是。这篇用一个 3 主集群,找一对「同节点、不同槽」的 key 来证明这一点,再看哪些命令会触发它、事务和 Lua 怎么处理,以及一个容易被忽略的 Lua 漏洞。

判据 = 槽
同一个节点上的两个 key,只要槽不同,多 key 命令仍然 CROSSSLOT
EXEC 整体拒绝
MULTI 里的 key 跨槽时,EXEC 报 CROSSSLOT,不会部分执行
同节点通过
Lua 里用 ARGV 传、没在 KEYS 里声明的 key,只要在本节点就不被拦(即使不同槽)

CROSSSLOT 不是「跨节点」

实测 · 本机执行输出
> MSET key0 1 key4 2
(error) CROSSSLOT Keys in request don't hash to the same slot
> MSET {t}1 1 {t}2 2
"OK"
> MGET {t}1 {t}2
1) "1"
2) "2"

选了 key0(槽 13252)和 key4(槽 13120):它们都在节点 7372 上,槽不同。直接发给 7372 的 MSET key0 1 key4 2 仍然报 CROSSSLOT Keys in request don't hash to the same slot。改成 hash tag {t}1、{t}2 就成功。原因不深:槽是集群迁移和路由的最小单位,服务器只保证「同一个槽的 key 会一起搬」。

触发 CROSSSLOT 的命令

实测 · 本机执行输出
> DEL key0 key4
(error) CROSSSLOT Keys in request don't hash to the same slot
> MGET key0 key4
(error) CROSSSLOT Keys in request don't hash to the same slot
> RENAME key0 key4
(error) CROSSSLOT Keys in request don't hash to the same slot
> SUNION key0 key4
(error) CROSSSLOT Keys in request don't hash to the same slot
> SADD key0 1
(integer) 1
> SINTERSTORE key4 key0 {q}1
(error) CROSSSLOT Keys in request don't hash to the same slot

所有「一条命令里涉及多个 key」的命令都要求它们同槽:DEL、MGET、RENAME、SUNION、SINTERSTORE……(SADD key0 1 只有一个 key,当然没问题。)

实测 · 本机执行输出
> SUNION key0 key0
1) "1"
> SCARD key0
(integer) 1

同一个 key 在一条命令里出现多次不算跨槽;单个 key 的命令(SCARD key0)当然也不会。

事务:EXEC 整体拒绝

实测 · 本机执行输出
> MULTI
"OK"
> SET key0 1
"QUEUED"
> SET key4 2
"QUEUED"
> EXEC
(error) CROSSSLOT Keys in request don't hash to the same slot
(改成同一个槽:)
> MULTI
"OK"
> SET {t}a 1
"QUEUED"
> SET {t}b 2
"QUEUED"
> EXEC
1) "OK"
2) "OK"

MULTI 之后的两条 SET 都返回 QUEUED,EXEC 时因为两个 key 不同槽整体被拒:CROSSSLOT,一条都没有执行。改成同槽的 {t}a、{t}b 之后正常提交。

Lua 脚本:KEYS 声明与绕过

实测 · 本机执行输出
> EVAL "return redis.call('SET', KEYS[1], 1) and redis.call('SET', KEYS[2], 2)" 2 key0 key4
(error) CROSSSLOT Keys in request don't hash to the same slot
> EVAL "return redis.call('SET', KEYS[1], 1) and redis.call('SET', 'undeclared', 2)" 1 key0
(error) ERR Script attempted to access a non local key in a cluster node script: 2f8a88effc3094ba5d57fc8e5724b35aa11e34d5, on @user_script:1.
(下面这条:没有在 KEYS 里声明的 key4,它和 key0 在同一个节点、不同槽)
> EVAL "redis.call('DEL', KEYS[1]); return redis.call('SET', ARGV[1], 2)" 1 key0 key4
"OK"
> EVAL "redis.call('SET', KEYS[1], 1); redis.call('SET', KEYS[2], 2); return 'ok'" 2 {t}x {t}y
"ok"

KEYS 里声明两个不同槽的 key → CROSSSLOT;声明同槽的 → 成功。更需要小心的是没有声明的 key:在脚本里直接写字符串或通过 ARGV 传入的 key,如果它属于别的节点,服务器报 Script attempted to access a non local key in a cluster node;但如果它和声明的 key 在同一个节点、只是槽不同(如 key4),脚本竟然执行成功。服务器只检查「是否在本节点」,不检查「是否同槽」。这样写的脚本在单个节点上能跑,但如果这个槽被迁移走,脚本会突然失败——所以脚本要访问的所有 key 都应该老老实实声明在 KEYS 里、并且同槽。

流水线与跨节点

实测 · 本机执行输出
['OK', '1', 2]

流水线里的多条单 key 命令没有限制——每条命令只涉及一个槽。

实测 · 本机执行输出
> MSET key0 1 key1 2
(error) CROSSSLOT Keys in request don't hash to the same slot

分属不同节点的两个 key 发 MSET 同样是 CROSSSLOT(而不是 MOVED)。

分步演示:CROSSSLOT 的边界

Redis Cluster 的 CROSSSLOT
本步的请求

    本文用到的工具与边界

    • 本机 macOS 27 / Redis 8.10.2 独立实例(本机回环端口,save 关闭),redis-cli 风格输出由自写的 RESP 客户端打印;计时用 time.perf_counter,数字每次运行会有小幅抖动。3 主 3 从;key0 / key4 / key1 是我从 key0…key199 里挑出的满足条件的样例
    • 没有验证:集群外的 Redis(单机)上同样命令的行为(单机没有槽的概念)、Redis 8 关于跨槽命令的新配置(如果有,本文没有涉及)、Lua 绕过声明的行为在迁移中槽上的具体表现(只在稳定集群测了)
    京ICP备2026067502号-1京公网安备11010502063487号