bigywater.tech
Redis · 70

MOVED 与 ASK 重定向

集群客户端最先遇到的两个错误是 MOVED 和 ASK,看起来都是「去别的节点问」,含义却完全不同。一个说「这个槽归它了,永远」,一个说「这一次去它那里问,别改你的地图」。这篇不靠概念,直接用 redis-cli 一样的原始命令,在一个正在迁移槽位的真实集群上,把两者出现的每一种情形都跑出来。

MOVED
槽的归属已经(永久)改变:客户端应更新槽位表
ASK
槽正在迁移,这一条命令去目标问:不要更新槽位表,先发 ASKING
TRYAGAIN
迁移中的多 key 命令,keys 分在源和目标两边

MOVED:请求发错了节点

实测 · 本机执行输出
> GET foo
"bar"
> GET foo
(error) MOVED 12182 127.0.0.1:7342
> SET foo x
(error) MOVED 12182 127.0.0.1:7342

槽 12182 属于 7342。发给另一个主库 7340 的 GET foo、SET foo x 都得到 MOVED 12182 127.0.0.1:7342:节点不会代办转发,只指路。

从库:READONLY

实测 · 本机执行输出
> GET foo
(error) MOVED 12182 127.0.0.1:7342
> READONLY
"OK"
> GET foo
"bar"

从库默认也回 MOVED。在连接上发一次 READONLY,之后这条连接上的读命令可以被从库自己处理(我只测了读命令)。

槽迁移的两端状态

实测 · 本机执行输出
> CLUSTER SETSLOT 12182 IMPORTING 75a351996149e140b8b7fb4cc22c69e642f91842
"OK"
> CLUSTER SETSLOT 12182 MIGRATING 33d348498d505ba72affc45ae4791ba0b034af81
"OK"
> CLUSTER COUNTKEYSINSLOT 12182
(integer) 2

迁移一个槽分两步设置:目标节点 CLUSTER SETSLOT 槽 IMPORTING 源节点ID,源节点 CLUSTER SETSLOT 槽 MIGRATING 目标节点ID。此时槽仍然属于源节点,槽里有 2 个 key(foo 和 {foo}exists,后者用 hash tag 落在同一个槽,第 72 篇)。

迁移中的源节点:ASK

实测 · 本机执行输出
> GET {foo}exists
"1"
> GET {foo}notthere
(error) ASK 12182 127.0.0.1:7340
> SET {foo}newkey 1
(error) ASK 12182 127.0.0.1:7340
> MGET {foo}exists {foo}notthere
(error) TRYAGAIN Multiple keys request during rehashing of slot

源节点对「自己还有的 key」照常服务(GET {foo}exists 得到 "1");对「自己没有的 key」——无论是读一个不存在的 key,还是新写入一个 key——回 ASK 12182 127.0.0.1:7340:这个 key 如果存在,就一定已经在目标那里了;新 key 也只能写到目标。MGET 同时带一个存在、一个不存在的 key,两边各有一部分,回 TRYAGAIN。

去目标节点:ASKING

实测 · 本机执行输出
> GET {foo}notthere
(error) MOVED 12182 127.0.0.1:7342
> ASKING
"OK"
> GET {foo}notthere
(nil)
> ASKING
"OK"
> SET {foo}newkey written-on-target
"OK"
ASKING 只对紧接着的一条命令有效:
> GET {foo}newkey
(error) MOVED 12182 127.0.0.1:7342

目标节点此时还不「拥有」这个槽,所以直接发命令会被打回 MOVED 指向源。必须先发 ASKING,紧接着的下一条命令才会被处理(GET 返回 nil、SET 成功)。ASKING 只管一条命令——第二次直接读同一个 key 又是 MOVED。所以 ASK 是「这一次」的指路,不应该更新客户端缓存的槽位表;MOVED 才是。

MIGRATE 与迁移完成

实测 · 本机执行输出
> MIGRATE 127.0.0.1 7340 {foo}exists 0 5000
"OK"
> GET {foo}exists
(error) ASK 12182 127.0.0.1:7340
> ASKING
"OK"
> GET {foo}exists
"1"
实测 · 本机执行输出
> MIGRATE 127.0.0.1 7340 foo 0 5000
"OK"
> CLUSTER SETSLOT 12182 NODE 33d348498d505ba72affc45ae4791ba0b034af81
"OK"
> CLUSTER SETSLOT 12182 NODE 33d348498d505ba72affc45ae4791ba0b034af81
"OK"
> GET foo
(error) MOVED 12182 127.0.0.1:7340
> GET foo
"bar"

MIGRATE 把 key 从源搬到目标(第 81 篇)。搬完一个 key,源上再读它也变成 ASK。把槽里的 key 都搬完之后,在两端各执行 CLUSTER SETSLOT 槽 NODE 目标ID 完成迁移:源节点从此回的是 MOVED,目标直接服务。(生产里用 redis-cli --cluster reshard 自动完成这些步骤,第 79 篇。)

分步演示:一个槽的迁移

MOVED 与 ASK
本步的请求

    本文用到的工具与边界

    • 本机 macOS 27 / Redis 8.10.2 独立实例(本机回环端口,save 关闭),redis-cli 风格输出由自写的 RESP 客户端打印;计时用 time.perf_counter,数字每次运行会有小幅抖动。3 主 3 从;节点 ID 从 CLUSTER NODES 取得;所有命令都是我手写客户端发的原始命令
    • 没有验证:迁移中源节点上某个 key 恰好正在被 MIGRATE 时的行为、客户端库对 ASK/MOVED 的具体实现差异、cluster-allow-reads-when-down 等选项
    京ICP备2026067502号-1京公网安备11010502063487号