Redis Cluster 在Spring中遇到的问题

Redis集群配置可能会在运行时更改。可以添加新节点,可以更改特定插槽的主节点。还有可能因为master宕机或网络抖动等原因,引起了主从切换。

无法感知集群槽位变化

SpringBoot2.x 开始默认使用的 Redis 客户端由 Jedis 变成了 Lettuce,但是当 Redis 集群中节点槽位变化之后,Lettuce 将无法继续操作 Redis,原因在于此时 Lettuce 使用的仍然是有问题的连接信息。

实际上,Lettuce 支持 redis 集群拓扑动态刷新,但是默认并没有开启,SpringBoot 在集成 Lettuce 时默认也没有开启。并且在 SpringBoot2.3.0 之前,是没有配置项设置 Lettuce 自动刷新拓扑的。在这次提交中增加了这一配置。使用Jedis便没有这个问题。

官方的描述Lettuce需要刷新节点拓扑视图Lettuce Github Wiki

解决方案

方法一:使用Jedis连接

Spring Boot2.0以下默认使用Jedis,由于jedis通过自身异常反馈来识别重连、刷新服务端的集群信息机制,保证其自动故障恢复,所以Jedis client默认自动支持拓扑刷新,方法一便是使用更换为Jedis客户端。

<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>

方法二:配置LettuceConnectionFactory,设置拓扑刷新策略

文档参考集群特定选项

@Bean
public DefaultClientResources lettuceClientResources() {
    return DefaultClientResources.create();
}

@Bean
public LettuceConnectionFactory lettuceConnectionFactory(RedisProperties redisProperties, ClientResources clientResources) {

    ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
            .enablePeriodicRefresh(Duration.ofSeconds(30)) //按照周期刷新拓扑
            .enableAllAdaptiveRefreshTriggers() //根据事件刷新拓扑
            .build();

    ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
            //redis命令超时时间,超时后才会使用新的拓扑信息重新建立连接
            .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(10)))
            .topologyRefreshOptions(topologyRefreshOptions)
            .build();

    LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
            .clientResources(clientResources)
            .clientOptions(clusterClientOptions)
            .build();

    RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(redisProperties.getCluster().getNodes());
    clusterConfig.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());
    clusterConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));

    LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(clusterConfig, clientConfiguration);

    return lettuceConnectionFactory;
}

方法三:开启自动拓扑刷新

Spring Boot2.3之后可以通过简单的配置变可以打开自动刷新拓扑的功能:

# 定时拓扑刷新(Periodic updates)
spring.redis.lettuce.cluster.refresh.period=60s
# 自适应拓扑刷新(Adaptive updates)
spring.redis.lettuce.cluster.refresh.adaptive=true

健康检查无法自动感知集群恢复

我们都知道Redis Cluster集群模式在主节点宕机后,会自动切换到可用的从节点,集群会再度恢复可用性。

但是如果在例如K8S、注册中心等管理服务中,存活探针用了actuator的health地址,那k8s容器里的服务也一样会down掉,也会导致服务不可用,即使服务层面已经刷新了redis集群的拓扑,服务/actuator/health健康情况依然会是down状态(原因是配置的redis集群nodes的每个node都会检查是否健康,不管这个node是主节点还是从节点),错误如下:

"redis": {
  "status": "DOWN",
  "details": {
    "error": "org.springframework.data.redis.RedisConnectionFailureException: Redis connection failed; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to 10.0.35.249:6380"
  }
},

redis cluster模式某节点宕机之后,Spring识别redis集群健康为down是个bug。是由于Spring Data Redis 2.2.8 提交所引起,具体可看这个解释。这个问题在spring boot2.4.x之后被修复

解决方案

方法一:升级Spring版本到2.4.X

作者有在issue#21514下回应到,不会在2.3.X版本修复这个问题,而是在2.4.X中才会修改

方法二:重写健康检查代码

重写redis集群健康监控的Indicator,可以参考issue#21514下某网友的回答:

// 重新实现RedisReactiveHealthIndicator

private Health up(Health.Builder builder, Properties info, ReactiveRedisConnection connection) {
        if (connection instanceof ReactiveRedisClusterConnection) {
            List<Map<String, String>> details = getDetails(info);
            if (details.isEmpty()) {
                return builder.outOfService()
                        .build();
            } else {
                return builder.up()
                        .withDetail("nodes", details)
                        .build();
            }
        } else {
            return builder.up()
                    .withDetail("version", info.getProperty("redis_version"))
                    .build();
        }
    }

    private List<Map<String, String>> getDetails(Properties info) {
        return info.keySet().stream()
                        .map(String.class::cast)
                        .map(k -> k.substring(0, k.lastIndexOf(".")))
                        .distinct()
                        .sorted()
                        .map(node -> Map.of(
                                "node", node,
                                "redis_version", info.getProperty(node + ".redis_version"),
                                "role", info.getProperty(node + ".role"),
                                "uptime_in_days", info.getProperty(node + ".uptime_in_days")
                                )
                        )
                        .collect(Collectors.toList());
    }

方法三:关闭Redis健康检查

management.health.redis.enabled=false

文档参考
RedisCluster集群模式下master宕机主从切换期间Lettuce连接Redis无法使用报错Redis command timed out的问题
redis集群拓扑结构自动更新:使用Lettuce连接Cluster集群实例时异常处理
刷新群集拓扑视图
Redis集群调整节点并手动切换主从引发的微服务报错问题
spring boot健康检查无法感知redis故障恢复的问题梳理
Redis集群模式下RedisReactiveHealthIndicator中断

相关推荐
失心疯_20232 分钟前
006.MySQL_查询数据
数据库·sql·mysql·关系型数据库·sqlyog·mysql教程·查询语句
环能jvav大师10 分钟前
基于R语言的统计分析基础:使用SQL语句操作数据集
开发语言·数据库·sql·数据分析·r语言·sqlite
骆晨学长30 分钟前
基于springboot的智慧社区微信小程序
java·数据库·spring boot·后端·微信小程序·小程序
@月落35 分钟前
alibaba获得店铺的所有商品 API接口
java·大数据·数据库·人工智能·学习
楠枬1 小时前
MySQL数据的增删改查(一)
数据库·mysql
goTsHgo1 小时前
从底层原理上解释 clickhouse 保证完全的幂等性
数据库·clickhouse
阿华的代码王国2 小时前
MySQL ------- 索引(B树B+树)
数据库·mysql
哈喽,树先生2 小时前
1.Seata 1.5.2 seata-server搭建
spring·springcloud
码爸3 小时前
flink 批量压缩redis集群 sink
大数据·redis·flink
Hello.Reader3 小时前
StarRocks实时分析数据库的基础与应用
大数据·数据库