
Redis Cluster 集群运维实战槽位迁移、MOVED/ASK 重定向与扩容缩容的数据一致性1. 从一个真实的告警说起凌晨两点监控系统弹出一条告警某核心业务的缓存命中率从 98% 掉到 71%接口平均响应时间上涨了 4 倍。值班同学登录集群查看发现集群状态是ok但客户端日志里密密麻麻地刷着MOVED和ASK重定向信息还有少量CLUSTERDOWN报错。进一步排查才发现白天运维同学为了扩容正在做槽位迁移迁移过程中客户端频繁重定向部分请求超时后被业务代码当成缓存未命中处理回源把数据库压高了。这个场景说明一件事Redis Cluster 的槽位迁移不是孤立操作它会直接改变客户端请求的路由路径进而影响延迟、一致性和业务行为。很多工程师会用redis-cli --cluster完成扩容但对“迁移时到底发生了什么”“MOVED 和 ASK 有什么区别”“迁移过程中数据会不会丢”“客户端怎么处理”这些问题没有建立起整体框架一旦线上出问题就很难快速定位。这篇文章的目标是先帮你在脑子里装一张 Redis Cluster 的“地图”然后沿着一次请求的完整路径把槽位迁移、MOVED、ASK、扩容缩容和数据一致性串起来。读完之后你应该能对着这张地图解释一个 key 存在哪、请求怎么走、迁移时状态怎么变、什么情况下会丢数据、客户端需要做什么。先记住一个最小模型Redis Cluster 把整个数据空间切成 16384 份每份叫一个槽位每个主节点负责其中一部分槽位客户端先算出 key 属于哪个槽再去找负责这个槽的节点。当槽位在节点之间搬家时客户端会被临时“指路”这就是 MOVED 和 ASK 的由来。2. 整体框架角色、连接与一次请求的流转2.1 集群里有哪些角色Redis Cluster 是去中心化的没有单独的控制平面。每个节点同时承担三重身份数据节点、总线成员、故障检测参与者。为了不把概念混在一起可以先把整体拆成三部分来看数据平面主节点master负责读写自己槽位内的数据从节点replica异步复制主节点数据主节点故障时从节点可被提升。元数据平面每个节点都保存一份集群配置clusterState记录 16384 个槽位分别由哪个节点负责以及所有节点的 IP、端口、角色和状态。通信平面集群总线Cluster Bus是节点之间的一条独立 TCP 连接默认端口是数据端口加 10000例如数据端口 6379总线端口 16379。它负责 gossip 协议交换节点信息、广播槽位归属变化、传播故障判定和配置纪元epoch。这里最容易误解的是集群总线不是用来传业务数据的业务请求走的是普通客户端连接节点之间复制走的是另一条复制连接。三条链路各司其职别把总线端口当成数据端口去连。2.2 一次请求的完整路径现在让一次请求完整走一遍你就能看到槽位、重定向和总线分别在什么位置介入客户端 目标节点A 目标节点B 集群总线 | | | | |-- GET user:1 ----| | | | (本地算槽5018) | | | | |-- 查本地集群配置 -| | | | 槽5018属于B | | |-- MOVED 5018 B --| | | | | | | |-- GET user:1 ------------------------| | | | |-- 命中返回值 ---| 返回数据 |-------------------------------------- 值 | | | | | | 后续槽位变化通过总线 gossip 扩散 ||步骤拆解客户端对 key 做 CRC16 计算再对 16384 取模得到槽位号。客户端在本地缓存的槽位映射表里查这个槽由谁负责直接连对应节点。如果客户端映射表过期连到了错误节点该节点会返回MOVED重定向客户端更新映射表后重试。如果该槽正在迁移源节点对已迁走的 key 返回ASK客户端临时转向目标节点但不更新本地映射表。槽位归属一旦正式变更会通过集群总线 gossip 扩散到所有节点最终客户端也会通过MOVED学到新映射。2.3 关键概念速查概念解决什么问题关键点槽位slot把 key 空间水平切分固定 16384 个CRC16(key) mod 16384集群总线节点间元数据同步端口 数据端口 10000gossip 协议配置纪元解决配置冲突谁的 epoch 大听谁的MOVED槽位已永久搬家客户端应更新本地槽位映射ASK槽位正在搬家临时转向不更新映射迁移状态标记槽位在搬MIGRATING/IMPORTING3. 槽位迁移谁在搬、怎么搬、搬到什么程度3.1 迁移的本质把 key 从一个节点搬到另一个节点槽位迁移说到底是把一个或多个槽位内的所有 key从源节点搬到目标节点。搬完之后这个槽位的归属正式变更。整个迁移是分阶段的不是一次性原子切换这一点决定了后面所有一致性问题。迁移前三方状态源节点负责某槽目标节点不负责该槽集群里其他节点也都认为该槽属于源节点。迁移中源节点把槽标记为MIGRATING目标节点把槽标记为IMPORTING此时该槽仍然“名义上”属于源节点但数据正在往目标节点搬。迁移完成源节点删除该槽的归属目标节点正式接管通过总线广播新配置。3.2 迁移涉及的命令运维常用的命令分两类高层封装命令和底层原子命令。高层命令由redis-cli --cluster封装底层命令由CLUSTER系列组成。# 高层命令把节点 B 加入集群然后把若干槽位从 A 迁到 Bredis-cli--clusteradd-node192.168.1.11:6379192.168.1.10:6379# 重新分片会交互式询问迁移多少槽、从哪个节点迁redis-cli--clusterreshard192.168.1.10:6379# 底层命令在目标节点上把槽 5018 设为导入状态redis-cli-h192.168.1.11-p6379cluster setslot5018importing源节点ID# 在源节点上把槽 5018 设为迁移状态redis-cli-h192.168.1.10-p6379cluster setslot5018migrating目标节点ID参数解释importing和migrating后面跟的是对端节点 ID不是 IP。节点 ID 可以用CLUSTER NODES查到。命令执行前要先确认两个节点都认识对方否则setslot会失败。3.3 一次迁移的流水线源节点A (MIGRATING 5018) 目标节点B (IMPORTING 5018) | | | 1. GETKEYSINSLOT 5018 100 ----------| (列出槽内 key) |------------ 返回 key 列表 -----------| | | | 2. MIGRATE host port key ... --------| 逐个/批量搬 key |------------ OK --------------------| | | | 3. 重复直到槽内无 key | | | | 4. CLUSTER SETSLOT 5018 NODE B ------| 双方都执行 | | | 5. 广播新配置到集群总线 --------------| 其他节点更新映射迁移过程中源节点对已迁走的 key 返回ASK对还没迁走的 key 正常处理目标节点对未迁完的 key只有在收到带ASKING标记的请求时才处理否则返回MOVED。3.4 迁移的边界与常见错误大 key 会让单次 MIGRATE 阻塞MIGRATE搬一个几百万元素的 Hash源节点会长时间阻塞影响同节点其他请求。建议迁移前用--bigkeys或MEMORY USAGE排查必要时先拆分大 key。迁移不是事务中途失败可能导致部分 key 已在目标节点、部分还在源节点此时槽位状态还是MIGRATING需要通过CLUSTER SETSLOT ... STABLE回滚或继续迁移。不要在业务高峰期迁移即使每个 key 很小迁移也会增加两个节点的 CPU 和网络负载重定向会放大客户端 QPS。4. MOVED 与 ASK两种重定向到底差在哪4.1 一句话区分先记住一句话模型MOVED 是“这个槽永久搬走了以后别再问我”ASK 是“这个 key 暂时在那边这次去那边取但先别改你的地图”。为什么需要两种重定向因为迁移期间槽位的“所有权”和“实际数据位置”会短暂分离。槽位名义上还属于源节点所以客户端映射表不能立刻改但有些 key 已经搬到目标节点了这次请求必须去目标节点取。ASK 就是为这个短暂窗口设计的。4.2 两种响应的对比维度MOVEDASK触发时机槽位已正式变更归属槽位正在迁移key 已搬到目标节点客户端动作更新本地槽位映射重试新节点不更新映射本次请求带ASKING转发目标节点是否持久是最终所有节点都会知道否迁移结束后不再出现常见错误客户端不刷新映射导致反复重定向忘记发ASKING目标节点返回 MOVED报文示例-MOVED 5018 192.168.1.11:6379-ASK 5018 192.168.1.11:63794.3 用 redis-cli 手动观察两种重定向准备一个三主三从集群人为把槽 5018 设为迁移状态就可以观察到两种响应# 1. 在目标节点 B 上设置导入redis-cli-h192.168.1.11-p6379cluster setslot5018importingA的节点ID# 2. 在源节点 A 上设置迁移redis-cli-h192.168.1.10-p6379cluster setslot5018migratingB的节点ID# 3. 在源节点 A 上写入一个还没迁走的 key正常返回redis-cli-h192.168.1.10-p6379setuser:1 v1# 4. 用 MIGRATE 把 user:1 搬到 Bredis-cli-h192.168.1.10-p6379migrate192.168.1.116379user:105000# 5. 此时客户端不直接连 B而是通过 A 访问 user:1会收到 ASKredis-cli-h192.168.1.10-p6379get user:1# 输出类似(error) ASK 5018 192.168.1.11:6379# 6. 客户端应发送 ASKING再发 GETredis-cli-h192.168.1.11-p6379asking redis-cli-h192.168.1.11-p6379get user:1# 输出v1# 7. 正式完成迁移后任何节点访问 user:1 都返回 MOVEDredis-cli-h192.168.1.10-p6379cluster setslot5018nodeB的节点IDredis-cli-h192.168.1.11-p6379cluster setslot5018nodeB的节点IDredis-cli-h192.168.1.10-p6379get user:1# 输出类似(error) MOVED 5018 192.168.1.11:6379关键点ASKING必须和真正的命令在同一条连接上、紧挨着发送它只对下一条命令生效。这就是为什么手写客户端时要特别小心连接池的复用如果ASKING和GET被分到不同连接上ASKING就白发了。这个例子能帮你理解两种重定向的差别但它不能替代真实客户端的状态机生产客户端还要处理连接失败、超时重试、映射表过期等复杂情况。4.4 Java 客户端怎么处理生产环境几乎不会手写重定向逻辑而是交给 Jedis、Lettuce、Redisson 这类客户端。以 Lettuce 为例它默认开启了自动重定向收到MOVED会刷新映射并重试收到ASK会发ASKING再转发。这里有一个最小可复现的示例。示例目标用 Lettuce 连一个三主三从集群观察MOVED自动处理与拓扑刷新。前置环境本机已启动一个三主三从集群端口 7000-7005。依赖io.lettuce:lettuce-core:6.3.2.RELEASE。importio.lettuce.core.RedisURI;importio.lettuce.core.cluster.RedisClusterClient;importio.lettuce.core.cluster.api.StatefulRedisClusterConnection;importio.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;importjava.util.Arrays;publicclassLettuceClusterDemo{publicstaticvoidmain(String[]args){RedisURIn1RedisURI.create(redis://127.0.0.1:7000);RedisURIn2RedisURI.create(redis://127.0.0.1:7001);RedisURIn3RedisURI.create(redis://127.0.0.1:7002);RedisClusterClientclientRedisClusterClient.create(Arrays.asList(n1,n2,n3));try(StatefulRedisClusterConnectionString,Stringconnclient.connect()){RedisAdvancedClusterCommandsString,Stringcmdconn.sync();for(inti0;i100;i){Stringkeyuser:i;cmd.set(key,vi);}System.out.println(写入完成get user:1cmd.get(user:1));}finally{client.shutdown();}}}关键步骤与预期结果客户端启动时用种子节点 7000-7001-7002 拉取集群拓扑然后本地维护槽位映射写入 100 个 key 会分布到三个主节点get user:1能直接命中。此时若在另一个终端执行redis-cli --cluster reshard你会看到部分请求在日志里触发重定向但结果仍然正确。容易改错的地方种子节点只需给部分节点Lettuce 会自动发现全量拓扑但如果你把种子节点写成单点且那个节点挂了初始连接就会失败所以生产至少配 3 个种子。另外Lettuce 默认autoReconnect和拓扑刷新是开启的不要随意关掉。5. 扩容加机器、迁槽位、扩散配置5.1 扩容要解决的核心问题扩容的目标通常是提升容量或吞吐内存不够了要加主节点分摊数据QPS 高了要加主节点分摊请求。扩容不是“加节点就完事”还需要把原来集中在老节点上的槽位重新分配否则新节点负责 0 个槽位等于白加。标准扩容分四步启动新节点用CLUSTER MEET或--cluster add-node把它加入集群。新节点此时是主节点但负责 0 个槽位需要把它的从节点也加进来。用--cluster reshard或手动MIGRATING/IMPORTING把部分槽位从老节点迁到新节点。等待集群总线 gossip 把新配置扩散到所有节点客户端刷新映射。5.2 一个可复现的扩容案例目标在一个三主三从集群中新增一个主从对并把 1000 个槽位从老主节点迁到新主节点。前置环境已有集群节点 7000-70057000/7001/7002 主7003/7004/7005 从。新增节点 7006主和 7007从配置文件已按集群模式启动。# 1. 把 7006 加入集群作为新主redis-cli--clusteradd-node127.0.0.1:7006127.0.0.1:7000# 2. 把 7007 加入集群并指定它是 7006 的从节点redis-cli--clusteradd-node127.0.0.1:7007127.0.0.1:7000 --cluster-slave --cluster-master-id7006的节点ID# 3. 查看当前槽位分配确认 7006 负责 0 个槽redis-cli--clustercheck127.0.0.1:7000# 4. 把 7000 上的一部分槽位迁到 7006# 交互式 reshard 会询问要迁移多少槽位、从哪个节点迁、迁到哪个节点redis-cli--clusterreshard127.0.0.1:7000\--cluster-from7000的节点ID\--cluster-to7006的节点ID\--cluster-slots1000\--cluster-yes# 5. 再次检查确认槽位分布和节点状态redis-cli--clustercheck127.0.0.1:7000预期结果--cluster check输出中7006 负责的槽位从 0 变成 1000集群状态为ok所有 16384 个槽位都有主节点负责。迁移过程中客户端会经历MOVED/ASK但业务不中断。关键点与边界--cluster-from和--cluster-to用的是节点 ID不是 IP。迁移的槽位数量要结合每个槽的平均数据量估算避免一次迁太多导致长时间重定向。reshard 过程中如果某个 key 特别大会拖慢整个迁移建议先处理大 key。如果集群开启了cluster-require-full-coverage yes只要有一个槽位没有主节点负责整个集群不可用扩容时要保持所有槽位始终有归属。5.3 扩容的流量影响扩容不是零成本的。新节点加入后客户端拓扑刷新会有延迟期间一部分请求可能仍然发到老节点并收到MOVED。假设某业务 QPS 是 5 万重定向比例 5%就多出 2500 次额外往返如果老节点此时负载已经很高重定向会进一步放大延迟。工程上建议低峰期扩容预留足够时间。每次迁移的槽位数量分批不要一把梭。观察cluster_stats_messages_sent、cluster_stats_messages_received等指标确认总线通信正常。6. 缩容下线节点前必须做的事6.1 缩容比扩容更容易出事缩容的直觉是“把节点删掉”但如果一个主节点上还有槽位或数据直接删会导致这部分槽位无人负责集群进入fail状态。正确顺序是先把槽位迁空再删除节点。缩容分为两种情况删除主节点和删除从节点。从节点没有槽位直接--cluster del-node即可主节点必须先reshard把它负责的槽位全部迁给其他节点确认它负责 0 个槽之后才能删除。6.2 一个完整的缩容案例目标把一个 4 主集群缩到 3 主安全下线节点 7006 及其从节点 7007。前置环境集群有 7000/7001/7002/7006 四个主节点7003/7004/7005/7007 四个从节点7006 负责 1000 个槽位。# 1. 确认 7006 负责哪些槽位redis-cli--clustercheck127.0.0.1:7000# 2. 把 7006 的 1000 个槽位全部迁给 7000# 注意 --cluster-from 是 7006--cluster-to 是 7000redis-cli--clusterreshard127.0.0.1:7000\--cluster-from7006的节点ID\--cluster-to7000的节点ID\--cluster-slots1000\--cluster-yes# 3. 再次确认 7006 负责 0 个槽位且集群状态 okredis-cli--clustercheck127.0.0.1:7000# 4. 删除从节点 7007redis-cli--clusterdel-node127.0.0.1:70077007的节点ID# 5. 删除主节点 7006redis-cli--clusterdel-node127.0.0.1:70067006的节点ID# 6. 最后确认集群拓扑和槽位覆盖redis-cli--clustercheck127.0.0.1:7000预期结果节点列表里不再有 7006 和 700716384 个槽位仍全部有主节点负责集群状态ok。容易出错的地方如果跳过第 2 步直接删 7006--cluster check会报[ERR] Nodes dont agree about configuration!或提示槽位未覆盖此时需要把残留槽位手动迁走或用CLUSTER SETSLOT ... STABLE恢复。删除节点前一定要确认它已经不再被其他节点当成槽位负责人。6.3 缩容期间的数据一致性缩容期间槽位在被迁走的节点上会经历MIGRATING在接收节点上是IMPORTING这和扩容完全对称。唯一不同的是下线节点的数据在迁移完成后就不再存在所以迁移必须完整执行完毕才能删除节点。如果迁移中途失败源节点上还有残留 key此时它的槽位状态可能仍是MIGRATING需要通过CLUSTER SETSLOT slot STABLE清理状态再决定重新迁移还是回滚。7. 集群总线重定向之外的隐藏主角7.1 总线到底传了什么前面反复提到“通过总线扩散配置”但集群总线具体传什么值得单独说清楚。每条总线消息有类型常见的有PING、PONG、MEET、FAIL、UPDATE、PUBLISH。其中UPDATE用来传播槽位配置变化FAIL用来传播节点故障判定MEET用来把新节点介绍给集群。节点之间按固定周期默认每秒若干次互发PING随机携带自己知道的其他节点信息这就是 gossip。gossip 的好处是去中心化、容错代价是配置扩散有延迟不是瞬间全局一致。7.2 总线消息与业务请求的关系可以用一个简单的类比把集群想象成一个办公室业务请求是同事之间直接对话总线消息是公告栏。槽位搬家相当于某个工位换了人公告栏需要一点时间才能更新。更新之前去旧工位的人会被口头告知“去新工位”这就是重定向。这个类比能帮你理解扩散延迟和重定向的来源但它不能替代真实的 epoch 机制配置冲突时节点是通过配置纪元大小来决定谁的信息更新的不是靠先来后到。7.3 总线相关的排障指标指标含义异常时可能的原因cluster_stats_messages_sent总线发送消息数突增可能因大量 FAIL/UPDATEcluster_stats_messages_received总线接收消息数与 sent 严重不对称说明网络问题cluster_known_nodes已知节点数持续增长说明有僵尸节点cluster_size负责槽位的主节点数与预期不符说明槽位未覆盖cluster_state集群状态fail通常因槽位未全覆盖8. 数据一致性迁移期间到底会不会丢数据8.1 先区分三种“一致性”聊 Redis Cluster 的数据一致性先要区分三个层次否则很容易鸡同鸭讲主从复制一致性主节点写入后异步复制到从节点主节点故障时可能丢失尚未复制的写入。这是 Redis 的复制模型决定的和 Cluster 无关。槽位迁移一致性迁移过程中同一个 key 不会同时存在于两个节点被同时读写靠的是MIGRATING/IMPORTING状态和重定向配合。客户端路由一致性客户端本地映射表可能滞后靠MOVED/ASK纠正最终一致而非实时一致。8.2 迁移期间的一次写入会经历什么假设槽 5018 正在从 A 迁到 B此时客户端写入user:1如果客户端直接连 AA 发现user:1还没迁走正常写入 A并异步复制给 A 的从节点。如果user:1已经迁到 BA 返回ASK客户端带ASKING去 B 写入数据落在 B。迁移完成后槽 5018 归属 B后续所有写入都去 BA 上已经没有该槽的 key。关键结论迁移过程中单个 key 的读写始终被路由到“当前实际持有它的节点”不会出现两个节点同时接受同一个 key 写入的情况。但如果你在迁移过程中直接绕过客户端、用固定连接分别往 A 和 B 写同一个 key就可能制造出两份数据这是人为错误不是集群机制的问题。8.3 迁移失败会不会丢数据MIGRATE是“搬完再删源端”的语义只有目标节点确认写入成功源节点才会删除本地 key。如果 MIGRATE 因为网络或目标节点失败而超时源节点会保留 key不会丢。但有一个边界要注意MIGRATE带COPY选项时是复制而非搬移源端不删不带COPY时才是搬移。使用默认行为时源端删除发生在目标端确认之后所以正常路径下不会丢。真正需要担心的是迁移过程中主节点宕机且从节点还没同步完这属于主从复制的一致性窗口不是槽位迁移特有的问题。8.4 一个用 Java 验证迁移期间读写不丢的示例目标在槽位迁移进行中持续对目标 key 做读写验证最终值和写入次数一致。前置环境三主三从集群user:hot所在槽位正在从 A 迁到 B。依赖 Jedis 集群客户端redis.clients:jedis:5.1.2。importredis.clients.jedis.HostAndPort;importredis.clients.jedis.JedisCluster;importredis.clients.jedis.DefaultJedisClientConfig;importjava.util.HashSet;importjava.util.Set;publicclassMigrationConsistencyDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{SetHostAndPortnodesnewHashSet();nodes.add(newHostAndPort(127.0.0.1,7000));nodes.add(newHostAndPort(127.0.0.1,7001));nodes.add(newHostAndPort(127.0.0.1,7002));DefaultJedisClientConfigcfgDefaultJedisClientConfig.builder().connectionTimeoutMillis(2000).socketTimeoutMillis(2000).build();try(JedisClusterclusternewJedisCluster(nodes,cfg)){for(inti0;i200;i){cluster.set(user:hot,vi);Stringgotcluster.get(user:hot);if(!(vi).equals(got)){System.out.println(不一致期望 vi 实际 got);}Thread.sleep(50);}System.out.println(最终值cluster.get(user:hot));}}}关键步骤与预期结果在另一个终端同时执行redis-cli --cluster reshard迁移user:hot所在槽位。示例代码持续写入 200 次并回读正常情况下每次读到的都是刚刚写入的值最终值是v199。中间可能出现MOVED/ASK重试导致单次延迟升高但不应该出现读到旧值。容易改错的地方Jedis 的JedisCluster默认最大重定向次数是 5如果迁移抖动剧烈可能会抛JedisClusterMaxAttemptsException生产上要结合重试策略业务侧兜底另外连接池参数过小会导致重定向期间排队间接放大延迟。9. 生产实践建议把机制变成可执行的操作规范9.1 扩容缩容的操作清单阶段动作检查项准备评估容量、QPS、大 key--bigkeys、MEMORY USAGE加入新节点 meet、加从节点--cluster check状态 ok迁移reshard 分批迁槽观察重定向率、延迟扩散等待总线同步cluster_known_nodes稳定收尾删除下线节点、核对拓扑16384 槽位全覆盖9.2 客户端配置建议至少配置 3 个种子节点避免单点导致初始拓扑拉取失败。开启拓扑刷新和自动重定向不要为了“稳定”手动关闭。设置合理的超时和最大重定向次数配合业务侧降级。连接池不要过小重定向期间会额外占用连接。9.3 监控要看什么重点关注重定向次数、cluster_state、cluster_slots_assigned、总线消息速率、各节点latency和used_memory。重定向率突然升高通常意味着有槽位在迁移或有客户端映射长期不更新。10. 常见误区与排障清单10.1 常见误区误区实际情况MOVED 和 ASK 可以互换处理MOVED 要更新本地映射ASK 只临时转发处理错会反复重定向迁移一定不丢数据正常路径不丢但迁移中主节点宕机仍可能因异步复制丢最近写入集群状态 ok 就没问题状态 ok 不代表迁移中无抖动重定向和延迟仍需观察直接删节点即可缩容主节点必须先迁空槽位否则集群可能进入 fail槽位迁移是原子的迁移分阶段中间状态可能残留需要处理 MIGRATING/IMPORTING10.2 排障清单按以下顺序排查集群异常1. CLUSTER INFO - 看 cluster_state、cluster_slots_assigned 2. CLUSTER NODES - 看节点角色、槽位、fail 标记 3. CLUSTER SLOTS - 看当前槽位到节点的映射 4. redis-cli --cluster check - 综合诊断集群一致性 5. 客户端日志 - 看重定向频率、超时、最大重试异常 6. 节点 slowlog/latency - 看是否有大 key 或阻塞命令 7. 总线指标 - 看消息速率、已知节点数是否异常每一步的判读标准cluster_state为fail时优先看cluster_slots_assigned是否为 16384CLUSTER NODES里出现fail?说明节点间对故障判定不一致通常是网络分区CLUSTER SLOTS与客户端映射不一致说明拓扑扩散还没完成稍等并触发一次刷新。11. 面试/复盘问题Redis Cluster 为什么是 16384 个槽位而不是 65536请从心跳包大小和集群规模角度回答。MOVED 和 ASK 分别由哪个节点、在什么状态下返回客户端处理方式有何不同槽位迁移过程中源节点和目标节点分别处于什么状态目标节点为什么需要ASKING才能响应请求如果迁移进行到一半源节点宕机了会发生什么数据会不会丢集群总线的端口怎么确定它和客户端连接、主从复制连接有什么区别缩容时为什么必须先迁空槽位再删除主节点如果顺序反了怎么恢复一次请求从客户端发出到返回可能经过哪些重定向路径画出来并说明每一步。12. 总结回到开头那张地图Redis Cluster 用 16384 个槽位把数据切开每个节点保存一份槽位映射节点之间靠集群总线 gossip 同步元数据。客户端先算槽位、再按映射路由映射过期时靠MOVED纠正槽位正在搬家时靠ASK临时转向。扩容就是加节点、迁槽位、扩散配置缩容就是先迁空槽位、再删节点。迁移过程中单个 key 的读写始终落在实际持有它的节点上正常路径不会丢数据但主从异步复制窗口和迁移失败仍需单独处理。如果你只记三句话槽位是路由单位总线是元数据通道MOVED/ASK 是客户端纠错机制。掌握这三点扩容缩容、重定向处理和一致性判断都能落到具体操作上。13. 参考资料Redis 官方文档Cluster Specificationredis.io/docs/reference/cluster-specRedis 官方文档CLUSTER SETSLOT、MIGRATE、ASKING、CLUSTER INFO、CLUSTER NODES 命令参考Redis 官方文档Redis Cluster Tutorialredis.io/docs/management/scalingLettuce 官方文档Redis Cluster 支持与拓扑刷新Jedis 官方文档JedisCluster 使用说明《Redis 设计与实现》黄健宏著关于集群与复制章节