一个MOVED错误引发的血案,我把Redis Cluster路由请求的原理翻了个底朝天
兄弟们,你们遇到过这种诡异报错吗?
MOVED 3999 192.168.1.2:6379——明明是连接的Redis,却返回了一个“搬家”指令,让你去另一个节点。第一次见到这个错误的时候,我整个人都懵了。后来我才明白,这正是Redis Cluster路由请求的核心机制。今天就从哈希槽计算、MOVED/ASK重定向,到Smart客户端的本地缓存,一次性讲透。
一、问题现场:为什么Redis让我“搬家”?
那天我们刚上Redis Cluster,业务方反馈说偶尔会有超时。我抓了一个请求日志,发现客户端抛出了这样的异常:
MOVED 3999 192.168.1.2:6379
意思很直白:你找的key不在我这儿,它在192.168.1.2:6379这个节点上,你去找它。
这就好比你去社区服务中心办业务,结果前台告诉你:“你这个业务不归我管,去3号窗口。”

二、第一步:键的哈希槽是怎么算的?
Redis Cluster将整个键空间划分为16384个哈希槽,每个key通过CRC16算法计算归属槽位:
slot = CRC16(key) & 16383
2.1 普通Key的计算
对于普通key,对整个字符串计算CRC16:
# 计算 user:1001 的槽位
slot = CRC16("user:1001") & 16383
# 假设结果是 3999
2.2 Hash Tag:让多个Key落在同一槽
Redis支持Hash Tag机制:只对{}内的内容计算哈希值。
# 以下两个key会落在同一槽
{user:1001}:name
{user:1001}:age
原理:只计算user:1001的哈希值,两个key的槽位相同。
为什么要设计Hash Tag?

适用场景:
-
需要同时操作多个key(如
MSET、事务) -
需要将关联数据放在同一节点
三、第二步:集群拓扑与槽位映射
每个节点都维护一份完整的集群状态表,记录每个槽位由哪个节点负责:
节点A: 槽 0-5460 节点B: 槽 5461-10922 节点C: 槽 10923-16383
客户端连接到集群后,会执行CLUSTER SLOTS命令获取这份映射关系。
> CLUSTER SLOTS 1) 1) 02) 54603) 1) "192.168.1.1"2) 63794) 1) "192.168.1.4"2) 6379 2) 1) 54612) 109223) 1) "192.168.1.2"2) 6379...

四、第三步:MOVED重定向——永久搬家
当客户端请求的key所对应的槽位不属于当前节点时,节点返回MOVED错误。

4.1 MOVED的本质
MOVED是一个永久性重定向。客户端收到后应该更新本地路由表,后续请求直接发往目标节点。
# MOVED响应格式 MOVED <slot> <ip>:<port>
4.2 源码分析:Redis如何返回MOVED?
// Redis Cluster 处理请求的核心逻辑(简化)
int processCommand(client *c) {// 计算key的哈希槽int slot = keyHashSlot(c->argv[1]->ptr, sdslen(c->argv[1]->ptr));// 检查槽是否属于当前节点if (server.cluster && slot != server.cluster->myslot) {// 找到槽对应的节点clusterNode *n = server.cluster->slots[slot];// 返回MOVED重定向addReplyError(c, "MOVED %d %s:%d", slot, n->ip, n->port);return C_ERR;}// 正常执行命令...
}
五、第四步:ASK重定向——临时借宿
5.1 ASK与MOVED的区别
ASK发生在槽位迁移过程中。当某个槽正在从节点A迁移到节点B时:
-
如果key还在节点A → 节点A直接处理
-
如果key已经迁移到节点B → 节点A返回
ASK,引导客户端去节点B

| 对比项 | MOVED | ASK |
|---|---|---|
| 含义 | 永久迁移 | 临时迁移(迁移过程中) |
| 客户端行为 | 更新本地路由表 | 不更新路由表,仅本次临时访问 |
| 原因 | 槽位已重新分配 | 槽位正在迁移中 |
5.2 为什么需要ASKING?
客户端收到ASK后,需要先发送ASKING命令,再发送真正的请求。
> GET user:1001 ASK 3999 192.168.1.2:6379 > ASKING +OK > GET user:1001 value
ASKING命令的作用是临时打开“借宿”权限——告诉目标节点:我来问一次,你让我查一下,但别把我的路由表改了。
// ASKING 命令的实现(简化)
void askingCommand(client *c) {// 设置一个标志,让目标节点允许执行一次跨槽请求c->flags |= CLIENT_ASKING;addReply(c, shared.ok);
}
六、第五步:Smart客户端——把重定向消灭在源头
6.1 什么是Smart客户端?
普通客户端每次请求都可能被重定向,增加一次网络往返。Smart客户端(如JedisCluster、Lettuce)在本地维护一份slot → node的映射表,大部分请求直接命中正确节点。
// JedisCluster 使用示例
JedisCluster jedis = new JedisCluster(new HostAndPort("192.168.1.1", 6379)
);// 直接操作,客户端内部自动路由
String value = jedis.get("user:1001");
6.2 Smart客户端的内部机制

6.3 为什么大部分时候不需要重定向?
Smart客户端的核心优势:
| 优势 | 说明 |
|---|---|
| 本地路由 | 避免每次请求都查询集群拓扑 |
| 自动更新 | 收到MOVED后自动刷新路由表 |
| 连接池 | 每个节点维护连接池,复用连接 |
| 故障转移 | 节点宕机时自动切换 |

七、网络分区与超时:路由失效的边界场景
7.1 集群拓扑变更的感知延迟
节点加入或宕机时,通过Gossip协议传播状态变更,需要秒级到十秒级的传播时间。Smart客户端在此期间可能发送请求到错误的节点,触发MOVED重定向并更新本地缓存。

7.2 全部节点不可用时的处理
如果所有主节点均不可用,客户端本地路由表虽然完整,但所有连接都会超时。Smart客户端会不断重试,直到集群恢复或抛出JedisClusterMaxAttemptsException。
八、整个路由流程全景

九、避坑总结
-
MOVED是永久重定向——客户端应该更新本地路由表,不能每次都去问
-
ASK是临时重定向——不更新路由表,多发生在扩容/缩容期间
-
Hash Tag不是万能的——过度使用会导致数据分布不均,热点集中
-
Smart客户端是必须的——生产环境不要用普通Jedis连接Cluster
-
网络分区时路由表可能失效——配合重试机制和业务兜底
兄弟们,你们在生产环境中遇到过Redis Cluster路由相关的问题吗?是MOVED风暴还是ASK重定向导致的超时?评论区聊聊,我帮你们分析分析。