二、问题现象
应用日志报错1:Lettuce 尝试连接这个IP,但连不上
io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: /10.2.1.119:7000 Caused by: java.net.ConnectException: Connection refused
应用日志报错2:Lettuce 认识这个IP,但拓扑视图里"不认
"2026-07-02 20:12:39.191 ERROR 1 [http-nio-40080-exec-7] [TID: cdca4378-bf8d-4e3a-bbb2-ad8809255aa2] UserExceptionHandler.exceptionHandler():96 - 服务端异常,异常信息={} org.springframework.data.redis.RedisSystemException: Redis exception; nested exception is io.lettuce.core.RedisException: io.lettuce.core.cluster.UnknownPartitionException: Connection to 10.2.1.119:7000 not allowed. This partition is not known in the cluster view.
三、根因分析
3.1 Jedis vs Lettuce
Spring Boot 2.x开始,默认的Redis客户端从 Jedis切换为 Lettuce。两者在集群拓扑刷新机制上存在关键差异:3.2 问题本质
Lettuce客户端在初始化时缓存了集群的拓扑信息(节点列表、slot分配等)。当7000节点宕机后:- Redis集群已完成故障转移,7005成为新master
- 但Lettuce客户端的拓扑视图 未更新,仍认为7000是master
- 操作 k1(slot在7000范围内)时,Lettuce继续尝试连接7000
- 连接失败后,客户端未触发拓扑刷新,陷入无限重试
四、解决方案
解决方案1:升级到 Spring Boot 2.3.0+(推荐)
Spring Boot 2.3.0开始,官方增加了对Lettuce拓扑刷新的配置支持。
步骤一:升级Spring Boot版本至2.3.0或更高。
步骤二:添加配置:
说明:
adaptive=true:根据 MOVED、 ASK、 PERSISTENT等事件触发刷新
period:定时刷新拓扑,作为兜底策略
dynamic-refresh-sources(Spring Boot 2.4.0+):决定刷新时只查初始种子节点(false)还是所有已发现节点(true)
adaptive=true 具体触发场景
Lettuce 的 AdaptiveRefreshTrigger 会在以下情况触发拓扑刷新:- MOVED 重定向
- 客户端收到 MOVED slot host:port 响应时
- 说明槽位已迁移,需要重新获取节点映射
- ASK 重定向
- 收到 ASK 响应时(槽位正在迁移中)
- 需要刷新以获取最新的迁移状态
- 连接断开/不可达
- 与某个节点的连接断开( ConnectionWatchdog 触发)
- 节点被判定为不可达时
- PERSISTENT 重连失败
- 连接断开后重试多次仍无法恢复
- UNCOVERED_SLOT / CLUSTERDOWN
- 收到 CLUSTERDOWN 错误(集群处于下线状态)
- 发现未覆盖的槽位( UNCOVERED_SLOT)
- READONLY 模式变更
- 从节点状态变化(如主从切换后)
关键源码逻辑(Lettuce)
// 触发条件枚举 enum Event { MOVED, // MOVED 重定向 ASK, // ASK 重定向 UNCOVERED_SLOT, // 槽位未覆盖 CLUSTERDOWN, // 集群下线 CONNECT, // 新连接建立(可选) DISCONNECT, // 连接断开 PERSISTENT_RECONNECTS // 持续重连失败解决方案2:通过代码配置LettuceConnectionFactory
适用于无法升级Spring Boot版本,或需要更精细控制的场景。
@Configuration public class LettuceRedisConfig { @Bean public DefaultClientResources lettuceClientResources() { return DefaultClientResources.create(); } @Bean public LettuceConnectionFactory lettuceConnectionFactory( RedisProperties redisProperties, ClientResources clientResources) { // 1. 配置拓扑刷新策略 ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder() .enablePeriodicRefresh(Duration.ofSeconds(30)) // 周期刷新 .enableAllAdaptiveRefreshTriggers() // 自适应刷新(全部触发源) .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(30)) .build(); // 2. 配置客户端选项 ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder() .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(10))) .topologyRefreshOptions(topologyRefreshOptions) .build(); // 3. 构建客户端配置 LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder() .clientResources(clientResources) .clientOptions(clusterClientOptions) .build(); // 4. 构建集群配置 RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(redisProperties.getCluster().getNodes()); clusterConfig.setMaxRedirects(redisProperties.getCluster().getMaxRedirects()); clusterConfig.setPassword(RedisPassword.of(redisProperties.getPassword())); return new LettuceConnectionFactory(clusterConfig, clientConfiguration); }}
@Bean(destroyMethod = "destroy") public LettuceConnectionFactory lettuceConnectionFactory() { // 拓扑刷新配置 ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder() .enableAllAdaptiveRefreshTriggers() .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(30)) .enablePeriodicRefresh(Duration.ofSeconds(20)) .build(); // 客户端选项 ClientOptions clientOptions = ClusterClientOptions.builder() .topologyRefreshOptions(topologyRefreshOptions) .build(); // 连接池配置 LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder() .poolConfig(genericObjectPoolConfig()) .clientOptions(clientOptions) .commandTimeout(redisProperties.getTimeout()) .build(); // 集群节点配置 RedisClusterConfiguration clusterConfig = buildClusterConfiguration(); return new LettuceConnectionFactory(clusterConfig, clientConfig); }
解决方案3:回退使用Jedis
适合不想折腾配置,或对Lettuce不熟悉的团队。
<dependency>
<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId> jedis</artifactId> </dependency>解决方案4:旧版本Spring Boot的兼容方案
对于Spring Boot 2.3.0之前的版本,可以参考方案2中开启 adaptive刷新的逻辑,手动配置 LettuceConnectionFactory。核心要点:- 必须开启 自适应刷新( enableAllAdaptiveRefreshTriggers())
- 建议同时开启 周期刷新( enablePeriodicRefresh())作为兜底
- 刷新频率不宜过高(建议10-60秒),避免对集群造成压力
五、配置项速查表
六、总结与建议
- 生产环境务必开启Lettuce拓扑刷新:无论通过配置还是代码,确保 adaptive=true
- 同时配置周期刷新作为兜底:防止自适应刷新因某些原因未能触发
- 合理设置刷新频率:生产环境建议30-60秒,避免过高频率压垮集群
- 升级到Spring Boot 2.3.0+:享受官方原生支持,减少维护成本