欢迎来到窥视未来的博客

https://github.com/lwx57280 https://gitee.com/li_VillageHead

一个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号窗口。”

mermaid-1785497600495

 

二、第一步:键的哈希槽是怎么算的?

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?

 

mermaid-1785497981340

 

适用场景

  • 需要同时操作多个key(如MSET、事务)

  • 需要将关联数据放在同一节点

三、第二步:集群拓扑与槽位映射

每个节点都维护一份完整的集群状态表,记录每个槽位由哪个节点负责:

节点A: 槽 0-5460
节点B: 槽 5461-10922
节点C: 槽 10923-16383

客户端连接到集群后,会执行CLUSTER SLOTS命令获取这份映射关系。

> CLUSTER SLOTS
1) 1) 0
   2) 5460
   3) 1) "192.168.1.1"
      2) 6379
   4) 1) "192.168.1.4"
      2) 6379
2) 1) 5461
   2) 10922
   3) 1) "192.168.1.2"
      2) 6379
   ...

mermaid-1785498059032

 

四、第三步:MOVED重定向——永久搬家

当客户端请求的key所对应的槽位不属于当前节点时,节点返回MOVED错误。

mermaid-1785498089313

 

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

mermaid-1785498185847

 

对比项MOVEDASK
含义 永久迁移 临时迁移(迁移过程中)
客户端行为 更新本地路由表 不更新路由表,仅本次临时访问
原因 槽位已重新分配 槽位正在迁移中

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客户端的内部机制

mermaid-1785498294512

 

6.3 为什么大部分时候不需要重定向?

Smart客户端的核心优势:

 
优势说明
本地路由 避免每次请求都查询集群拓扑
自动更新 收到MOVED后自动刷新路由表
连接池 每个节点维护连接池,复用连接
故障转移 节点宕机时自动切换

mermaid-1785498378479

 

七、网络分区与超时:路由失效的边界场景

7.1 集群拓扑变更的感知延迟

节点加入或宕机时,通过Gossip协议传播状态变更,需要秒级到十秒级的传播时间。Smart客户端在此期间可能发送请求到错误的节点,触发MOVED重定向并更新本地缓存。

mermaid-1785498432380

 

7.2 全部节点不可用时的处理

如果所有主节点均不可用,客户端本地路由表虽然完整,但所有连接都会超时。Smart客户端会不断重试,直到集群恢复或抛出JedisClusterMaxAttemptsException

八、整个路由流程全景

image

 

九、避坑总结

  1. MOVED是永久重定向——客户端应该更新本地路由表,不能每次都去问

  2. ASK是临时重定向——不更新路由表,多发生在扩容/缩容期间

  3. Hash Tag不是万能的——过度使用会导致数据分布不均,热点集中

  4. Smart客户端是必须的——生产环境不要用普通Jedis连接Cluster

  5. 网络分区时路由表可能失效——配合重试机制和业务兜底

兄弟们,你们在生产环境中遇到过Redis Cluster路由相关的问题吗?是MOVED风暴还是ASK重定向导致的超时?评论区聊聊,我帮你们分析分析。

posted on 2026-07-31 20:06  k8s-Mango  阅读(0)  评论(0)    收藏  举报

导航