1. 先搞清楚这个异常到底在说什么去年做一次压测的时候凌晨两点多监控面板上突然一阵红色日志里刷得最快的一行就是redis.clients.jedis.exceptions.JedisMovedDataException: MOVED 8742 10.0.0.12:6379。当时业务侧的现象很简单一部分请求正常另一部分请求偶发 500抛出的就是这个JedisMovedDataException。前后折腾了差不多一天才彻底理清后来又在几个不同项目里反复踩过同类问题所以这篇文章我打算把这件事从头到尾讲透——它是什么、为什么会冒出来、怎么在客户端和服务端两侧同时处理干净以及我在实际改造过程中记录下来的参数选择和踩坑细节。不管你是刚接手一个老项目、第一次把单机 Redis 换成集群模式还是已经在集群上跑了一段时间突然开始报这个错下面的内容都能直接对照着排。先把最基础的一层说清楚。Redis 集群把 16384 个哈希槽分散在多个主节点上每个主节点只负责一部分槽。当客户端把一条命令发到了不该管这个 key的节点上那个节点不会帮你转发它只会回一个错误字符串格式是-MOVED slot ip:port意思是这个槽归后面这个地址管你去那边问。Jedis 在解析服务端返回的错误前缀时识别到MOVED这个标记就把它包装成JedisMovedDataException抛给调用方。所以严格来讲这个异常不是 Redis 坏了也不是网络断了它是一张你找错人了去隔壁窗口的指路条只不过客户端要么没接住这张条子要么压根没打算按条子走。理解这一点非常关键因为它决定了后面所有的排查方向问题几乎总是出在客户端有没有能力跟随重定向上而不是出在服务端凭什么重定向上。1.1 一个真实感很强的翻车现场我遇到过最典型的一次是一个运行了两年多的老服务。早期它是单机 Redis代码里用的是JedisPool包了一层工具类全项目几百处调用都走这个工具类。后来数据量涨了运维把实例换成了三主三从的集群模式连接地址给了一个 VIP应用侧只改了一行配置文件把 IP 换掉就上线了。头两天没出问题因为压测流量小、命中的 key 恰好落在客户端连上的那个节点负责的槽里。等到正式放量大概是原先流量的六倍立刻开始零星报JedisMovedDataException而且是这个 key 报、那个 key 不报很有迷惑性。迷惑点在于很多人第一反应是某个 key 有问题于是拿着那个 key 去查数据、查过期时间、查是否有大 key。其实跟 key 本身毫无关系。JedisPool底层的Jedis对象是一条直连某个节点的普通连接它完全不知道集群拓扑的存在也没有槽位映射表。它把命令发出去对方回MOVED它除了把异常往上抛之外没有别的选择。所以命中的 key 是不是有问题根本不重要重要的是这个 key 归哪个槽、这个槽归哪个节点、而你的连接连的是哪个节点——三者不一致就会报一致就不报。这就解释了为什么现象看起来是随机的。一个快速验证的方法把报错日志里的 key 拿出来用redis-cli -c -h 节点 -p 端口 get key试一下。带-c参数时客户端会自动跟随重定向如果能正常取到值基本可以确定就是客户端没有跟随重定向的能力而不是数据或集群本身出了故障。1.2 MOVED 和 ASK 不是一回事别混着处理搞这个异常绕不开的另一个概念是ASK。它俩长得很像格式都是标记 槽号 目标地址但语义完全不同处理方式也不能一样。MOVED表示槽的归属已经永久变更了客户端应该更新本地缓存的槽位映射表以后这个槽的所有请求都发到新地址去。ASK表示槽正在迁移过程中这个 key 已经被搬到了目标节点但槽本身还没正式交割客户端这次请求需要先给目标节点发一个ASKING命令再发真正的命令而且不能更新本地缓存——因为迁移结束后这个槽可能还会回到原节点也可能去别的地方。Jedis 里对应的是JedisMovedDataException和JedisAskDataException两者都继承自JedisRedirectionException都能通过getTargetNode()拿到目标地址。用JedisCluster的时候这两种情况它内部都处理掉了你基本看不到用裸Jedis的时候两种都会抛出来。区分它们很重要因为处理策略不一样MOVED是改地址簿ASK是临时问一嘴。如果在自己的重试逻辑里把ASK当成MOVED去刷新缓存在槽迁移期间会造成缓存频繁抖动严重的会让客户端在迁移窗口里反复横跳延迟曲线拉出一个锯齿。1.3 这个异常家族的成员对照排查的时候光盯着一个异常名容易走偏我把 Jedis 在集群场景下常见的几个异常整理成一张表遇到问题时先对号入座能省掉大量猜测时间。异常类触发场景本质原因处理方向JedisMovedDataException命令发到了非槽主节点客户端不知道槽位归属换JedisCluster或刷新拓扑JedisAskDataException槽迁移中key 已搬到目标节点迁移窗口期的临时重定向发ASKING后重试不刷缓存JedisNoReachableClusterNodeException启动时所有种子节点都连不上地址、端口、密码、网络、安全组检查连通性与认证配置JedisClusterMaxAttemptsException重试次数耗尽仍失败重定向次数超限或集群持续抖动调大maxAttempts同时查集群健康度JedisConnectionException连接超时、连接被重置网络、连接池耗尽、服务端拒绝查池参数、超时、服务端连接数JedisDataException: NOAUTH密码缺失或错误认证失败配置密码与 MOVED 无关表格里最后一行特意放进来是因为我见过不止一次有人把NOAUTH也当成集群配置问题去查拓扑。认证失败和槽位重定向完全是两条线先把异常 message 看清楚再动手比什么都重要。2. 根因拆解四条最常见的触发路径JedisMovedDataException表面上只有一种表现背后其实是四五条完全不同的路径。搞清楚自己的项目落在哪条上改造方案才有意义。下面这四条是我这些年在不同项目里实际遇到过的按出现频率从高到低排。2.1 用单机客户端连集群新手最常踩的第一脚这是绝对的高频场景没有之一。表现为应用启动正常小流量下一切正常流量一上来就开始报而且报错的 key 分布看起来很随机。根因就是前面说的JedisPool提供的是直连连接不具备槽位路由能力。很多人会问那我配了多个节点地址行不行比如spring.redis.cluster.nodes里写了一串地址是不是就行了这里有个容易被忽略的细节在 Spring Boot 里spring.redis.cluster.nodes这个配置项只对集群模式的客户端生效如果你同时保留了spring.redis.host和spring.redis.port某些版本的自动配置会因为属性优先级或条件注解的判断最终装配出来的是一个单机连接工厂。配置文件里两套属性并存、看起来都配了应该没问题实际生效的却是单机那一套这个坑我至少见过三次每次都是靠打印实际注入的 Bean 类型才定位到。还有一种变体项目里同时引入了两个 Redis 客户端依赖一个是比较新的、支持集群的一个是老的工具类依赖业务代码里图省事直接用了老工具类。这种情况下编译不报错、启动不报错只在特定 key 上出错排查成本极高。我的建议是接手任何项目先做一件事——把JedisPool、Jedis、JedisCluster这几个类名的引用全项目搜一遍看清楚到底有几种客户端在跑。2.2 拓扑变了客户端还揣着旧地图即便你用的是JedisCluster这个异常也不是完全不会出现。JedisCluster内部维护一份槽位到节点的缓存第一次连上时会从种子节点拉取CLUSTER SLOTS建立这张表。此后如果集群发生主从切换、节点扩容、槽迁移这张表就过期了。JedisCluster的设计是懒刷新只有当你把一个请求发到了错误的节点、收到MOVED之后它才会去刷新缓存然后重试。所以在拓扑变更的那一瞬间你会看到少量MOVED相关的日志这是正常的。不正常的是两种情况。第一种刷新频率过高说明集群一直在抖可能是有节点反复上下线也可能是网络分区导致频繁选主。第二种永远刷不过来每次请求都在重定向最后抛JedisClusterMaxAttemptsException。后者往往意味着客户端看到的地址和服务端返回的地址对不上——比如服务端返回的是内网真实地址而客户端只能访问另一套地址重定向之后连不上重试几次耗尽。较新版本的 Jedis 提供了周期性刷新拓扑的能力配置项名字里带topologyRefresh之类的关键字可以让客户端按固定间隔主动拉取一次槽位映射而不是等到报错才被动刷新。这个能力在集群经常变动的环境里很有价值具体配置项名称和可用性以你手上那个版本的官方说明为准各小版本之间有差异不要照抄博客里的写法。我自己的做法是先确认版本再去看对应版本的构造器和配置类能省掉一堆编译不过的麻烦。2.3 槽迁移窗口期正常重定向被当成故障集群扩容或者缩容的时候运维要把一部分槽从一个节点搬到另一个节点。迁移过程是逐步的先标记源节点的槽为MIGRATING、目标节点的槽为IMPORTING然后逐个 key 搬过去全部搬完之后才正式交割。在这个窗口里客户端访问已经搬走的 key 会收到ASK访问还没搬的 key 正常返回访问已经交割完成的槽会收到MOVED。这个阶段的异常量取决于两件事迁移的 key 数量和客户端的处理能力。如果一次迁几十万个 key窗口可能持续十几分钟客户端会一直处在重定向状态。用JedisCluster的话这些都被吞掉了你只会在监控上看到延迟的小幅上升用裸Jedis的话就是满屏异常。我一般建议把大规模槽迁移安排在业务低峰并且分批做一次迁一两个槽观察一轮再继续。图快一次性迁完客户端扛不住反而更容易出事。还有一种更隐蔽的情况迁移做了一半操作用的会话断开了槽卡在MIGRATING状态没人收尾。这时候集群状态其实是不完整的客户端会持续收到ASK表现为某些 key 的访问延迟明显高于其他 key。这种情况得靠CLUSTER NODES的输出里有没有残留的[slot--nodeid]标记来发现正常情况下这个标记应该很快消失。2.4 中间挂了透明代理地址对不上这个坑比较高级但踩到就很疼。有些环境会在 Redis 集群前面放一层四层代理做统一入口客户端连的是代理地址。问题在于服务端返回的MOVED里带的地址是节点自己的真实地址不是代理地址。客户端拿到这个地址去连如果网络不通就会变成连接超时如果勉强通了但走的不是同一条路径就会在重定向和超时之间反复横跳日志里异常和超时交替出现。判断方法很简单把MOVED后面那个地址抄下来在应用服务器上直接telnet ip port试一下能不能通、通到的是不是你以为的那个东西。如果不通或者通到的不是同一个服务那就别在客户端层面折腾了先解决地址可达性问题。稳妥的做法是让客户端直连所有集群节点或者在代理层做完整的地址映射保证服务端返回的地址在客户端侧是可路由的。3. 方案选型三条路各自的代价知道原因之后可选的处理方式其实就那么几条各有各的适用场景和代价。我按照推荐程度排一下顺便说说为什么不推荐某些看起来很省事的做法。3.1 路线 A换成 JedisCluster一劳永逸这是标准答案也是我在所有新项目里的默认选择。JedisCluster内部维护槽位缓存、自动处理MOVED和ASK、自动重试、连接池按节点分别管理你要做的就是正确初始化它并给它合适的参数。改造工作量取决于项目里有多少地方直接用了Jedis类型——如果原来就有统一的工具类封装通常只需要改工具类内部实现业务代码一行不动如果是散落各处的直接调用那就得先把调用收敛到统一入口这个重构本身也是好事。需要提醒的是JedisCluster并不是绝对不抛JedisMovedDataException。在拓扑剧烈变动、缓存还没刷新过来的极短窗口里理论上你仍然可能在日志里瞥见它但JedisCluster内部会捕获并重试正常情况下不会抛到你的业务代码。如果业务代码里 catch 到了这个异常那说明重试次数已经耗尽问题就不在客户端了得去查集群。3.2 路线 B保留单机连接手动跟随重定向有些项目因为历史包袱太重短期内换不了客户端就想着在工具类里 catch 住JedisMovedDataException解析出目标地址再新建一条连接发过去。这个思路能跑通但我把它归为应急手段不建议长期保留。原因有三。第一它只处理了MOVED没处理ASK槽迁移期间照样出问题迁移中的 key 需要先发ASKING再发命令少这一步就会收到新的重定向。第二它没有缓存槽位信息每次重定向都要新建连接等于把集群的槽位映射每次都重新发现一遍QPS 一高连接数就爆。第三JedisMovedDataException.getMessage()的格式在不同版本间可能变化靠字符串解析拿目标地址很脆弱虽然getTargetNode()这个方法是稳定的但很多人写代码时图省事直接切字符串。应急可以别当成终态方案。3.3 路线 C服务端收敛减少重定向的发生前两条都是客户端侧的动作其实服务端也有可以做的事。最直接的就是合理设计 key 的命名。Redis 集群支持 hash tag用花括号把 key 的一部分括起来只有花括号里的内容参与槽位计算。比如order:{10086}:detail和order:{10086}:status会落到同一个槽这样就避免了同一笔订单的两个 key 在不同节点、需要跨节点操作的麻烦。hash tag 用好了能解决很多问题批量操作不用跨槽、Lua 脚本可以同时操作多个 key、事务能正常开。但它是一把双刃剑用不好会造成数据倾斜——所有订单都打在同一个 tag 上那部分槽全压在一个节点其他节点闲着。设计的时候要把 tag 的粒度想清楚是按用户 ID 分还是按订单号分基数够不够大热点会不会集中。3.4 三条路线的对比维度路线 AJedisCluster路线 B手动重定向路线 Chash tag 收敛改造工作量中需收敛调用入口小小到中涉 key 设计是否处理 ASK自动处理需自己实现易漏不影响性能影响可忽略连接数上升明显视 tag 设计而定长期可维护性好差好适用场景所有集群环境紧急止血需要多 key 原子操作我的推荐度首选仅应急配合 A 使用实际项目里路线 A 和路线 C 通常是搭配用的客户端换JedisCluster解决重定向服务端用 hash tag 解决跨槽操作。路线 B 只在线上正在炸、今晚必须止血、改造来不及的时候用而且要在事后补上整改任务。4. 实操从零改造一个扛得住重定向的 Jedis 客户端下面这部分是具体动手的内容。我用一个典型的三主三从集群做例子假设节点地址是10.0.0.11:6379、10.0.0.12:6379、10.0.0.13:6379带密码认证应用侧用的是 Jedis 4.x 系列。不同小版本的构造器签名会有增减我下面给的写法以我本机能编译通过的版本为准你照抄之前先在 IDE 里看一眼构造器提示参数含义是一致的只是顺序或重载不同。4.1 依赖与版本选择版本这件事要单独说一句因为它直接影响到你能用哪些能力。Jedis 3.x 时代的JedisCluster构造器参数用的是裸的int超时和字符串密码配置项少、灵活性差4.x 引入了JedisClientConfig把超时、认证、SSL、客户端名这些统一收进一个 builder代码干净很多5.x 又进一步把集群相关的配置单独抽出来支持周期刷新拓扑这类能力。跨大版本升级的时候构造函数基本都会变不要拿旧代码直接套新版本。依赖本身没什么花头关键是要确认项目里只有这一个 Redis 客户端依赖别出现新旧并存。dependency groupIdredis.clients/groupId artifactIdjedis/artifactId version4.4.6/version /dependency版本号你可以按自己项目的依赖管理来我写这个只是给个参照。加完之后用mvn dependency:tree | grep jedis看一眼确认没有别的传递依赖把老版本拽进来——这种情况在引入了第三方 SDK 的项目里很常见最后跑起来的版本可能跟你在pom.xml里写的不一样。4.2 初始化 JedisCluster 的正确姿势先看代码然后我逐段解释为什么这么写。import redis.clients.jedis.HostAndPort; import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisClientConfig; import redis.clients.jedis.DefaultJedisClientConfig; import redis.clients.jedis.Connection; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import java.time.Duration; import java.util.Arrays; import java.util.HashSet; import java.util.Set; public class RedisClusterFactory { public static JedisCluster build() { // 种子节点不用全配但建议至少给 2 到 3 个 SetHostAndPort seeds new HashSet(Arrays.asList( new HostAndPort(10.0.0.11, 6379), new HostAndPort(10.0.0.12, 6379), new HostAndPort(10.0.0.13, 6379) )); // 连接池配置每个节点一份 GenericObjectPoolConfigConnection poolConfig new GenericObjectPoolConfig(); poolConfig.setMaxTotal(64); poolConfig.setMaxIdle(16); poolConfig.setMinIdle(4); poolConfig.setMaxWait(Duration.ofMillis(1000)); poolConfig.setTestOnBorrow(true); poolConfig.setBlockWhenExhausted(true); // 客户端配置 JedisClientConfig clientConfig DefaultJedisClientConfig.builder() .password(your-password) .connectionTimeoutMillis(2000) .socketTimeoutMillis(2000) .clientName(order-service) .build(); return new JedisCluster(seeds, clientConfig, poolConfig); } }种子节点这里有个细节值得说JedisCluster初始化时只要有一个种子节点能连上就能拉取到完整的拓扑所以理论上配一个就够。但我建议至少配两个原因很实际——如果恰好配置的那个节点在应用启动时正在重启或者处在故障转移过程中客户端直接启动失败整个服务起不来。配三个基本能覆盖掉单节点故障的概率。反过来说也不需要把所有节点都配上配太多反而拉长初始化时的探测时间。clientName这个参数很多人不填其实挺好用的。填上之后在服务端执行CLIENT LIST就能看到每条连接属于哪个应用排查到底是谁在疯狂重定向的时候特别省事。生产环境上我一般会把服务名和实例编号拼进去。至于setMaxTotal(64)这个值别照着抄得按你的实际情况算下一节展开。4.3 连接池与重试参数怎么算连接池参数拍脑袋定是常态但算一下其实不费事。基础公式是需要的并发连接数 ≈ 峰值 QPS × 单次命令平均耗时秒。假设某个服务峰值 2000 QPS单次GET平均 2 毫秒那么同时处于在途状态的请求大约是 2000 × 0.002 4 条。乘一个 1.5 到 2 的冗余系数再留一些给突发流量maxTotal给到 16 到 32 就相当宽裕了。很多人的直觉是往大了配总没错配到 200、500。这样做的后果是服务端连接数暴涨Redis 单线程处理命令连接多了上下文切换和内存开销都上去了反而拉低整体吞吐。而且连接池太大还有一个隐患服务端出问题时大量连接同时超时重试形成雪崩。我的经验值是按上面公式算出来之后再乘 2够用就行不够的话看监控里的numWaiters指标再调。maxAttempts重试次数这个参数更值得细说因为它直接和超时时间耦合。假设socketTimeout是 2000 毫秒maxAttempts配成 10那么最坏情况下一次调用要经历 10 次尝试耗时可能接近 20 秒。如果你的接口本身超时预算只有 3 秒那这个配置等于把线程池往死里拖——请求都堵在等 Redis 上线程耗尽整个服务不可用。所以我的一般建议是maxAttempts给 3 到 5配合 1 到 2 秒的socketTimeout最坏情况可控在几秒内。真遇到集群大面积故障快速失败比慢慢重试更有价值。// 常见的参数组合按接口的超时预算反推 int connectionTimeout 1000; // 建连超时内网可以短一些 int socketTimeout 1500; // 读超时按 P99 的 2 到 3 倍给 int maxAttempts 4; // 3 到 5 之间别贪大参数之间存在联动关系改一个必须回头看另一个。尤其是把maxAttempts调大的时候一定要重新算一遍最坏耗时别让 Redis 的重试把上游接口的超时预算吃光。4.4 Spring Boot 项目的配置落地如果用 Spring Boot 的自动配置配置项要先确认版本。2.x 系列是spring.redis.cluster.*3.x 系列改成了spring.data.redis.cluster.*写错前缀不会报错只是不生效然后你就收获一个单机客户端和满屏MOVED。spring: data: redis: password: your-password timeout: 1500ms connect-timeout: 1000ms cluster: nodes: - 10.0.0.11:6379 - 10.0.0.12:6379 - 10.0.0.13:6379 max-redirects: 4 lettuce: pool: max-active: 32 max-idle: 16 min-idle: 4 max-wait: 1000ms注意这里我故意写的是lettuce因为 Spring Boot 2.x 之后默认的 Redis 客户端是 Lettuce 而不是 Jedis。如果你的项目确实要用 Jedis得把 Lettuce 依赖排掉、显式引入 Jedis并且确认自动配置真的切过去了。怎么确认启动之后把RedisConnectionFactory这个 Bean 注入进来打印一下类名是JedisConnectionFactory还是LettuceConnectionFactory一目了然。这个动作看着傻但它能一次性排掉配置写对了但根本没生效这类问题。还有一个高频坑spring.data.redis.host和spring.data.redis.cluster.nodes同时存在。前者会让自动配置走单机分支后者被忽略。我见过有人在配置文件里留了一行注释掉的host结果某次合并分支把它复活了线上直接出问题。清理配置的时候不用的一定删掉别注释。4.5 复现与验证先制造问题再确认修好改造完之后怎么验证真的修好了我习惯先造一个能稳定复现的环境再验证修复。做法很简单用裸Jedis连集群的某个非 owner 节点故意访问一个不属于它的 key。// 这段代码就是为了复现异常不要用在生产 try (Jedis jedis new Jedis(10.0.0.11, 6379)) { jedis.auth(your-password); // 换几个不同的 key总有一个不在 10.0.0.11 负责的槽上 for (int i 0; i 20; i) { String key repro: i; try { System.out.println(key - jedis.get(key)); } catch (JedisMovedDataException e) { System.out.println(key 触发重定向目标节点 e.getTargetNode()); } } }跑一遍能看到部分 key 正常返回null因为不存在部分 key 抛出JedisMovedDataException并打印出目标地址。这说明复现环境搭好了。然后把同样的 key 用JedisCluster跑一遍应该全部正常返回没有任何异常。两步对比做完改造是否生效就非常确定了。命令行侧也可以用redis-cli验证带不带-c的区别很明显# 不带 -c收到重定向错误就直接打印出来 redis-cli -h 10.0.0.11 -p 6379 get repro:7 # 返回(error) MOVED 866 10.0.0.12:6379 # 带 -c客户端自动跟随 redis-cli -c -h 10.0.0.11 -p 6379 get repro:7 # 返回- Redirected to slot [866] located at 10.0.0.12:6379 # (nil)这个小实验花不了五分钟但能让你对重定向到底长什么样有非常直观的认识比看十篇文档都管用。以后日志里再出现这个异常你脑子里立刻就有画面了。5. 集群运维侧的配合动作客户端改完了不等于万事大吉。我见过客户端明明用的是JedisCluster却还是频繁重定向的情况最后查下来是集群本身一直在抖。所以这一节讲讲服务端该看什么、该防什么。5.1 上线前先做一次集群体检任何一次涉及集群变更的上线扩容、缩容、迁移、升级我都建议先跑一遍下面这组命令把结果存下来做基线。# 集群整体状态重点看 cluster_state 是不是 ok redis-cli -h 10.0.0.11 -p 6379 -a your-password cluster info # 节点列表重点看有没有 fail 标记、槽有没有覆盖全 redis-cli -h 10.0.0.11 -p 6379 -a your-password cluster nodes # 槽位分布确认 16384 个槽全部有主 redis-cli -h 10.0.0.11 -p 6379 -a your-password cluster slotscluster info里要盯三个值cluster_state必须是okcluster_slots_assigned必须是 16384cluster_known_nodes应该等于你的节点总数。任何一个不对说明集群处在不完整状态这时候上业务就是在给自己找事。cluster nodes的输出要重点看每个节点的 flags 字段正常情况下主节点是myself,master或者master从节点是slave出现fail、fail?、noaddr都是要处理的信号。另外要确认每个主节点后面的槽区间是连续的、合起来正好覆盖 0 到 16383有缺口就说明有槽没人管客户端访问到这些槽会直接报CLUSTERDOWN。5.2 槽迁移期间的客户端动作迁移这事儿服务端做的是搬运客户端做的是跟随两边得配合。我的经验是把迁移拆成小批次每次迁一两个槽观察客户端监控里的异常率和延迟曲线稳定了再迁下一批。迁之前先确认cluster-require-full-coverage这个参数是什么值——如果设成yes默认那么只要有一个槽不可用整个集群都会拒绝服务设成no的话只有落到不可用槽的请求会失败其他槽不受影响。生产环境上这个值怎么设要看业务能不能接受部分不可用我个人倾向保守一点保持默认然后靠迁移前充分准备、迁移中密切观察来保证稳定。迁移过程中如果发现有槽卡在MIGRATING状态迟迟不结束先别急着强行收尾。用CLUSTER GETKEYSINSLOT slot count看看还剩多少 key 没搬用CLUSTER COUNTKEYSINSLOT slot数一下总量。如果剩的 key 很少但搬不动往往是某个大 key 迁移超时了可以单独针对它调大MIGRATE命令的超时参数。如果剩的 key 很多说明迁移任务本身断了得重新发起。5.3 值得配置的监控项客户端侧我一般会监控三个指标JedisMovedDataException和JedisAskDataException的出现次数、单次命令的 P99 延迟、连接池的numWaiters。前两个直接反映重定向频率正常情况下应该接近零突然抬头就说明拓扑在变延迟和等待数反映客户端是否被拖住池子里排队说明连接不够或者服务端慢了。服务端侧重点看cluster_state的变化、各节点的connected_clients、instantaneous_ops_per_sec、keyspace_hits/misses比例、以及每个节点的内存使用率是否均衡。集群最怕的不是单点慢是分布不均——某个节点的槽特别多或者大 key 特别多它会先扛不住然后引发故障转移再引发客户端的一轮重定向。6. 常见问题速查与踩坑记录最后一节把我这些年记录的典型问题和排查路径整理出来遇到情况直接查表比从头捋一遍快得多。6.1 问题速查表现象最可能的原因快速验证方式处理动作小流量正常放量后偶发 MOVED用了单机客户端连集群搜代码里的JedisPool引用换JedisCluster启动就报NoReachableClusterNode地址、端口、密码、安全组telnet加redis-cli -a试连修正连接配置报MOVED但目标地址连不上服务端返回真实地址客户端走代理在应用机上telnet目标地址解决地址可达性或改直连重定向持续不断最后MaxAttempts集群持续抖动或地址错配看cluster nodes的 flags先修集群再看客户端某些 key 延迟明显高槽卡在MIGRATING状态cluster nodes找[slot--id]收尾迁移或重新迁移报CROSSSLOT而非MOVED多 key 命令跨了槽看 key 有没有统一 hash tag加花括号统一 tagNOAUTH和MOVED混着出现密码没配或配置未生效打印实际 Bean 类型修正密码与自动配置6.2 几个我踩过的坑坑一只配一个种子节点赶上它重启就启动不了。有一次发版应用启动时正好撞上运维在重启那个唯一的种子节点结果整个服务起不来回滚了一次。后来把种子节点改成三个这个问题再没出现过。代价几乎为零收益很明显。坑二以为调大maxAttempts能提高成功率。早期配置改成了 10想着多试几次总能成功。结果集群一次短时抖动接口 P99 从 20 毫秒飙到十几秒线程池打满上游跟着雪崩。后来老实改回 4同时在业务层加了降级逻辑效果反而好得多。Redis 挂了就让它挂别把整个服务拖下水。坑三集群模式下还在用KEYS和跨 key 的MGET。集群模式不支持跨槽的多 key 操作MGET里如果几个 key 落不到同一个槽直接报CROSSSLOT。以前单机时代随手写的MGET代码迁移到集群后全得改。要么用 hash tag 把 key 归到一槽要么改成 pipeline 分批取。坑四SELECT数据库。集群模式只支持 0 号库SELECT 1直接报错。老代码里如果有切库逻辑迁移到集群前必须清理掉不然会以各种奇怪的方式失败。坑五依赖版本被传递依赖拉低。pom.xml里写的是新版本跑起来却是老版本构造函数对不上编译报错或者运行时报NoSuchMethodError。上线前跑一次依赖树把冲突排掉省事。坑六日志里只打异常 message 不打目标节点。JedisMovedDataException的 message 里带了槽号和目标地址但很多人只把 message 拼进日志没注意到它其实还能通过getTargetNode()拿到结构化的地址信息。排查的时候把这两个都打出来尤其是目标节点能一眼看出是所有 key 都指向同一个节点还是分散指向多个节点这两种情况的根因完全不同。6.3 我常备的排查命令清单把下面这几条存成一个小脚本出问题时按顺序跑一遍能覆盖八成的情况。# 1. 集群整体状态 redis-cli -h host -p port -a pwd cluster info # 2. 节点与槽分布找 fail 标记和残缺区间 redis-cli -h host -p port -a pwd cluster nodes # 3. 某个 key 到底归哪个槽、哪个节点 redis-cli -h host -p port -a pwd cluster keyslot key # 4. 该槽当前的归属比 keyslot 更直接 redis-cli -h host -p port -a pwd cluster slots # 5. 带重定向跟随的访问测试 redis-cli -c -h host -p port -a pwd get key # 6. 看当前连接都是谁配合 clientName 用 redis-cli -h host -p port -a pwd client list | grep 服务名我个人在实际操作中的体会是遇到JedisMovedDataException这个异常最快的定位路径不是去翻代码而是先跑第 2 条和第 4 条命令确认槽位归属和节点健康状态。如果集群本身是健康的、槽位覆盖完整、没有 fail 节点那问题百分之百在客户端直接去看用的哪个客户端类、配置的哪个前缀反过来如果集群本身就不健康那客户端怎么改都是治标先把集群弄稳再说。这个判断顺序能帮你省掉大量来回猜测的时间。另外一个小技巧改完客户端之后别急着全量发布先挑一台机器灰度把日志级别调到 DEBUG观察半小时里JedisMovedDataException的出现次数。如果是从每分钟几百次变成零那就可以放心推了如果还有零星出现但总量下降到个位数那属于拓扑刷新的正常窗口可以接受如果只是从几百降到几十那说明还有别的链路在用单机客户端继续搜。
阅读完成 · 觉得有帮助?