上一篇【第48篇】哈希槽——Redis Cluster的数据分片机制下一篇【第50篇】集群重新分片——不停服迁移槽位的黑魔法前面我们搞懂了节点怎么认识彼此、数据怎么分散到槽。但现在有个灵魂拷问客户端发来一个GET user:1001当前节点如果发现这key不在自己这里该怎么办答案藏在两个字里重定向。Redis Cluster定义了两种重定向——MOVED和ASK。它们长得像但性格完全不同。理解它们的区别是使用Cluster的及格线。一、键查找的完整流程从CRC16到命中或重定向当一个命令到达某个节点时查找流程如下客户端发送: GET user:1001 │ ▼ ┌─────────────────────────────┐ │ 1. 计算slot │ │ slot CRC16(user:1001) │ │ 16383 │ │ → slot 8416 │ └──────────────┬────────────┘ ▼ ┌─────────────────────────────┐ │ 2. 查本节点槽位图 │ │ 调用 clusterState.slots[] │ │ slots[8416] ? │ └──────────────┬────────────┘ ▼ ┌────────┴────────┐ ▼ ▼ slots[8416] slots[8416] myself ! myself │ │ ▼ ▼ ┌──────────┐ ┌─────────────────┐ │ 直接处理 │ │ 3. 返回重定向 │ │ GET/PASS │ │ MOVED 或 ASK │ └──────────┘ └─────────────────┘关键代码在src/cluster.c中的getNodeByQuery()函数。它做三件事// 伪代码简化版clusterNode*getNodeByQuery(client*c,...){// 1. 提取key计算slotintslotkeyHashSlot(key,keylen);// 2. 查slot映射表clusterNode*nserver.cluster-slots[slot];// 3. 判断返回类型if(nserver.cluster-myself){returnmyself;// 命中}// 4. 检查是否MOVED还是ASKif(server.cluster-migrating_slots_to[slot]){// slot正在迁出 → ASK}else{// slot永久不属于我 → MOVED}}整个过程O(1)完成——CRC16计算是常数时间数组下标访问也是常数时间。这也是Redis Cluster能保持高性能的关键。二、MOVED重定向此槽已搬家永久地MOVED是最常见的重定向。当slot永久不属于当前节点时返回MOVED错误127.0.0.1:6379SET hello world(error)MOVED866127.0.0.1:6380格式为MOVED slot ip:port。它的语义是“你需要的slot 866现在已经永久归127.0.0.1:6380管了以后所有这个slot的请求都直接找它别再找我了。”MOVED的触发条件触发 MOVED 的场景 slot[i] 指向的节点 ! myself 且 migrating_slots_to[i] NULL 没有在迁出 且 importing_slots_from[i] NULL 没有在迁入 简单说这个槽本来就不归我管你找错人了。MOVED的内部处理收到MOVED后节点不只是返回错误还会在错误信息中附带上目标节点的IP和端口。客户端拿到后通常做两件事更新本地槽路由表把slot的映射关系更新到正确的节点向目标节点重发请求直接连目标节点执行客户端 Node A Node B │ │ │ │ GET user:1001 │ │ │ ─────────────────────→ │ │ │ │ slot查表 → 属于Node B │ │ ←── MOVED 8416 B:6380 ── │ │ │ │ │ │ 更新路由表: slot 8416 → B │ │ │ │ │ │ GET user:1001 │ │ │ ──────────────────────────────────────────────→ │ │ │ │ ←── value ─────────────────────────────────── │踩坑提示如果用普通redis-cli不带-c连接Cluster节点每次MOVED都是冷冰冰的错误信息。必须手动重连到目标节点。带-c的redis-cli会自动处理重定向开发调试时一定记得加上-c。三、ASK重定向槽正在搬家暂住这儿ASK和MOVED长得像但语义完全不同。当slot正在迁移过程中数据可能暂时在目标节点上这时返回的是ASK127.0.0.1:6379GET migrating_key(error)ASK3999127.0.0.1:6380关键差异维度MOVEDASK语义永久搬迁槽永远归目标节点临时借住槽还在迁移中何时触发slot归属明确已变更slot正在importing/migrating客户端行为更新本地槽映射直接连目标节点不更新槽映射先发ASKING再发请求查询前不需要额外命令必须先执行ASKING命令后续请求全部发给目标节点只有这次会话发给目标节点用途正常路由迁移期间访问已迁移的key对应状态无IMPORTING目标节点/ MIGRATING源节点持久性永久迁移完成后消失打个比方MOVED “你的快递地址改了以后都送到新地址”搬新家了ASK “师傅去新地址发货了你今天去新地址找他”临时出差四、ASKING命令ASK重定向的通行证ASK重定向有个特殊要求客户端重连到目标节点后必须先发送ASKING命令再发送真正的请求。客户端 Node A (6379) Node B (6380) │ │ │ │ GET key │ (slot正在迁移中) │ │ ─────────────────→ │ │ │ │ importing_slots_from[slot]│ │ │ → 指向 Node B │ │ ← ASK slot B:6380 ── │ │ │ │ │ │ ASKING │ │ │ ────────────────────────────────────────────→ │ │ │ │ accepting slot │ │ │ 的importing状态 │ GET key │ │ │ ────────────────────────────────────────────→ │ │ │ │ key已迁过来了 │ ← value ───────────────────────────────────── │为什么需要ASKING因为Node B虽然被设为slot的importing目标但它在正常情况下会拒绝不属于自己slot的请求。ASKING就是告诉Node B“我知道这个slot按理说还不归你管但它的数据可能已经到你这里了你帮我查一下。”ASKING的作用域只限一次请求。发完一个请求后下次又得重新ASKING。这保证了迁移完成后客户端不会一直往错误的地方发请求。// 源码中处理ASKING的逻辑简化if(c-flagsCLIENT_ASKING){// ASKING标志允许访问正在importing的slotc-flags~CLIENT_ASKING;// 用完就清除// 允许访问}踩坑提示ASKING命令在redis-cli -c模式下是自动发的但如果用其他客户端库需要正确处理ASK跳转。错误的做法是不发ASKING就发请求——Node B会直接返回MOVED因为它的slots[]数组里这个slot还不指向自己造成死循环。五、键迁移状态机IMPORTING与MIGRATING理解ASK重定向必须先理解键迁移状态机。当一个槽在节点A和节点B之间迁移时两个节点各自维护一个状态┌─ Node A (源节点) ─────────┐ ┌─ Node B (目标节点) ───────┐ │ │ │ │ │ migrating_slots_to │ │ importing_slots_from │ │ [slot] Node B │ │ [slot] Node A │ │ │ │ │ │ 状态MIGRATING │ │ 状态IMPORTING │ │ │ │ │ │ 行为 │ │ 行为 │ │ ┌─── 如果key还在本地 │ │ ┌─── 收到ASKING后 │ │ │ → 正常处理 │ │ │ → 接受请求并查找key │ │ └─── 如果key已迁移走 │ │ │ │ │ │ → 返回 ASK │ │ └─── 没有ASKING的请求 │ │ └──────────────────── │ │ │ → 返回 MOVED │ │ │ │ └──────────────────── │ └────────────────────────────┘ └────────────────────────────┘状态机图迁移开始 ┌──────────┐ │ 正常状态 │ │ slots[slot]│ │ Node A │ └─────┬────┘ │ CLUSTER SETSLOT slot IMPORTING B CLUSTER SETSLOT slot MIGRATING A │ ┌─────┴──────┐ ┌───────┤ 迁移中状态 │ │ └─────┬──────┘ ▼ │ ┌───────────┐ │ key逐个迁移... │ Node B │ │ │ IMPORTING │ │ │ 收到请求: │ │ │ 有ASKING? │ │ 有→处理 │ │ │ 无→MOVED │ │ └───────────┘ │ ▼ ┌──────────────┐ │ Node A │ │ MIGRATING │ │ 收到请求: │ │ key在本地? │ │ 有→处理 │ │ 无→ASK │ └──────────────┘ │ 所有key迁移完毕/CLUSTER SETSLOT NODE │ ┌──────┴──────┐ │ 迁移完成 │ │ slots[slot] │ │ Node B │ └─────────────┘迁移中访问一个key的完整时序客户端 Node A (MIGRATING) Node B (IMPORTING) │ │ │ │ GET key_X │ │ │ ──────────────→│ │ │ │ key_X已迁移走了 │ │ ← ASK slot B ─ │ │ │ │ │ │ ASKING │ │ ──────────────────────────────────→ │ │ │ │ │ GET key_X │ │ ──────────────────────────────────→ │ │ │ │ │ │ │ key_X在这里 │ ← value ────────────────────────── │六、客户端的槽路由缓存与更新策略客户端的性能很大程度取决于槽路由的缓存策略。每次都让服务端重定向就太慢了。缓存的更新时机启动时 │ ▼ ┌────────────────────┐ │ 通过 CLUSTER SLOTS │ │ 获取完整槽位映射 │ └─────────┬──────────┘ │ ▼ ┌────────────────────┐ │ 本地缓存 │ │ slotTable[16384] │ └─────────┬──────────┘ │ 请求key命中槽 │ ┌─────────┴──────────┐ ▼ ▼ 命中了缓存 MOVED错误 │ │ ▼ ▼ 直接请求 更新该slot的映射 重试请求具体规则收到MOVED→ 立即更新该slot的映射到新节点收到ASK→ 不更新缓存只重发一次因为槽还在迁移映射没变CLUSTER SLOTS→ 定时刷新Jedis默认每分钟一次Lettuce会根据MOVED自动刷连接断开→ 清理该节点的槽映射缓存Jedis 的槽处理// JedisCluster 内部逻辑简化版try{// 直接执行命令returnjedis.get(key);}catch(JedisMovedDataExceptione){// MOVED错误更新槽缓存重试HostAndPorttargetNodee.getTargetNode();connectionHandler.renewSlotCache();returnexecuteCommand(targetNode,key);}catch(JedisAskDataExceptione){// ASK错误发送ASKING重试不更新缓存HostAndPorttargetNodee.getTargetNode();jedis.asking();returnjedis.get(key);}Lettuce 的槽处理Lettuce对Cluster的支持更加细腻// Lettuce自动处理MOVED/ASK重定向RedisClusterClientclientRedisClusterClient.create(RedisURI.create(redis://127.0.0.1:6379));StatefulRedisClusterConnectionString,Stringconnectionclient.connect();RedisAdvancedClusterCommandsString,Stringsyncconnection.sync();// Lettuce内部会// 1. 启动时加载CLUSTER SLOTS// 2. 收到MOVED自动更新分区视图并重试// 3. 收到ASK自动发送ASKING并重试// 4. 异步刷新槽映射sync.set(key,value);// 完全透明两者的对比特性JedisLettuceMOVED处理抛出异常应用层处理自动重试透明ASK处理抛出异常应用层处理自动重试透明槽缓存刷新定时刷新可配自适应刷新连接管理连接池有状态连接池共享连接线程安全否需JedisPool是原生多路复用踩坑提示用Jedis的老项目中常见一个bug捕获了JedisMovedDataException但没有更新缓存导致每次请求都先发到旧节点再重定向。这等于每次请求都要两个RTT性能直接腰斩。七、redis-cli -c 的自动重定向实现redis-cli -c是调试Cluster的利器。它的内部逻辑其实很简单redis-cli -c 的重定向处理: 发送命令 │ ▼ 收到响应 │ ├── 正常响应 → 直接输出 │ ├── MOVED slot ip:port │ │ │ ├─ 更新本地的slot→节点映射 │ ├─ 连接到新节点 │ ├─ 在新连接上重新发送命令 │ └─ 输出结果 │ └── ASK slot ip:port │ ├─ 连接到目标节点不更新映射 ├─ 发送 ASKING ├─ 重新发送命令 └─ 输出结果注意一个关键细节redis-cli -c在收到MOVED时会切换连接——之后的所有命令都走新连接。这是因为CLI默认连接模式是绑定到一个节点切换后能避免无止境的重定向循环。# 演示重定向过程 $ redis-cli -c -p 6379 127.0.0.1:6379 GET hello - Redirected to slot [866] located at 127.0.0.1:6380 world 127.0.0.1:6380 GET hello # 注意提示符变了已经切到6380 world 127.0.0.1:6380 GET foo - Redirected to slot [12182] located at 127.0.0.1:6381 bar 127.0.0.1:6381 # 又切到了6381总结MOVED和ASK是Redis Cluster客户端交互中最精妙的设计。MOVED表示你找错人了这是新地址永久重定向ASK表示数据可能暂时在那边你先去问问临时跳转。理解它们的区别就是理解了Cluster在正常状态和迁移状态下如何保持数据可访问性。MOVED是常态ASK是迁移态。日常使用中90%的重定向都是MOVED只有在线扩缩容时才遇到ASK。但一旦用ClusterASK就一定会遇到——数据不会平白无故地从32GB变64GB必须迁移。八、实战排查MOVED/ASK常见问题问题1客户端不断收到MOVED重定向性能暴跌症状应用日志里看到大量 JedisMovedDataExceptionQPS 从 10万 降到 3万 原因槽映射缓存失效或未更新 - 可能是集群刚完成reshard客户端没及时刷新CLUSTER SLOTS - 可能是Jedis连接池中某个连接还连着旧节点 解决 1. 检查客户端日志确认MOVED指向的节点是否正确 2. 手动触发槽映射刷新JedisCluster.renewSlotCache() 3. 重启应用端确保所有连接池刷新 4. 设置更短的槽映射刷新周期问题2ASK重定向后得到MOVED陷入循环症状客户端日志显示ASK → MOVED → ASK → MOVED → ... 原因迁移状态机混乱 - 目标节点importing状态丢失比如节点重启 - 迁移中途操作失误导致slot状态不一致 解决 1. CLUSTER NODES | grep -E importing|migrating 检查状态 2. 如果发现不一致手动用CLUSTER SETSLOT清掉残留的迁移状态 3. 重新执行reshard问题3Lettuce的PauseDetector触发后恢复慢Lettuce有个高级特性叫拓扑刷新暂停检测器TopologyRefreshPauseDetector当集群状态变化时短暂暂停请求。如果配置不当可能触发过于频繁// Lettuce集群连接配置建议ClusterClientOptionsoptionsClusterClientOptions.builder().topologyRefreshOptions(TopologyRefreshOptions.builder().enablePeriodicRefresh(Duration.ofSeconds(30))// 30秒刷新一次.enableAllAdaptiveRefreshTriggers()// 收到MOVED/ASK自动刷新.build()).maxRedirects(5)// 最多重定向5次.validateClusterNodeMembership(true)// 验证节点成员身份.build();关键参数说明maxRedirects防止无限重定向。设为3-5比较合适设太小可能在正常reshard时失败enableAllAdaptiveRefreshTriggers最重要的开关启用后收到任何重定向都会触发拓扑刷新refreshPeriod定期刷新间隔太短浪费资源太长更新不及时。30秒是个好的折中如何验证重定向是否正确工作# 1. 在迁移过程中持续观察重定向whiletrue;doredis-cli-c-p6379CLUSTER INFO|grep-Estate|slots_ok|slots_pfailsleep2done# 2. 用一个key测试重定向链redis-cli-c-p6379DEBUG OBJECTtest_key21# 3. 用redis-benchmark在集群模式下测试redis-cli-c-p6379--clustercall192.168.1.101:6379 PING# 4. 模拟一个MOVED响应# 先看slot归属redis-cli-p6379CLUSTER KEYSLOTtest_key# 向错误的节点发请求redis-cli-p6380GETtest_key# 应该返回MOVEDredis-cli-c-p6380GETtest_key# 应该自动重定向下一篇文章我们将直接上手实战——如何在不停止服务的情况下把槽从一个节点迁移到另一个节点。CLUSTER SETSLOT、MIGRATE、redis-cli --cluster reshard整条迁移链路一文讲透。上一篇【第48篇】哈希槽——Redis Cluster的数据分片机制下一篇【第50篇】集群重新分片——不停服迁移槽位的黑魔法