Spring Boot集成redis集群拓扑动态刷新

项目场景:

Spring Boot集成Redis集群,使用lettuce连接Cluster集群实例。


问题描述

redis其中一个节点挂了之后,springboot集成redis集群配置信息没有及时刷新,出现读取操作报错。

java.lang.IllegalArgumentException: Connection to 127.0.0.1:6379 not allowed. This connection point is not known in the cluster view
exceptionStackTrace
io.lettuce.core.cluster.PooledClusterConnectionProvider.getConnectionAsync(PooledClusterConnectionProvider.java:359)
io.lettuce.core.cluster.ClusterDistributionChannelWriter.write(ClusterDistributionChannelWriter.java:93)
io.lettuce.core.cluster.ClusterCommand.complete(ClusterCommand.java:56)
io.lettuce.core.protocol.CommandHandler.decode(CommandHandler.java:563)
io.lettuce.core.protocol.CommandHandler.channelRead(CommandHandler.java:516)

原因分析:

lettuce默认是没有开始拓扑更新及读写分离导致的


解决方案:

这里分为几种情况:

  1. springboot 1.x之前版本默认使用jedis,无需要手动开启刷新
  2. springboot 2.x默认为Lettuce,需要代码设置开启刷新节点拓扑策略
  3. springboot 2.3.0开始,支持集群拓扑刷新功能,属性配置开启即可

第一种情况:springboot1.x版本环境

springboot1.x之前版本默认使用jedis,无需手动开启动态刷新。

第二种情况:springboot2.0~2.3版本环境

springboot2.0-2.3版本默认使用lettuce,默认不支持属性配置集群拓扑刷新。使用lettuce,需要增加配置类,需要手动开启刷新。

 配置类如下:

package com.test.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import lombok.extern.java.Log;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.lang.reflect.Method;
import java.time.Duration;


@Log
@Configuration
@EnableCaching // 开启缓存支持
public class RedisConfig {

    @Autowired
    private RedisProperties redisProperties;

    @Bean(destroyMethod = "destroy")	//销毁这个bean之前调用这个destroy回调方法释放资源
    public LettuceConnectionFactory redisConnectionFactory() {
        // redis单节点
        if (null == redisProperties.getCluster() || null == redisProperties.getCluster().getNodes()) {
            RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(redisProperties.getHost(),
                redisProperties.getPort());
            configuration.setPassword(redisProperties.getPassword());	//RedisPassword.of(redisProperties.getPassword())
			configuration.setDatabase(redisProperties.getDatabase());
            return new LettuceConnectionFactory(configuration);
        }

        // redis集群
        RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(redisProperties.getCluster().getNodes());
        redisClusterConfiguration.setPassword(redisProperties.getPassword());	//RedisPassword.of(redisProperties.getPassword())
        redisClusterConfiguration.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());

        GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
        genericObjectPoolConfig.setMaxTotal(redisProperties.getLettuce().getPool().getMaxActive());
        genericObjectPoolConfig.setMaxIdle(redisProperties.getLettuce().getPool().getMaxIdle());
        genericObjectPoolConfig.setMinIdle(redisProperties.getLettuce().getPool().getMinIdle());
        genericObjectPoolConfig.setMaxWaitMillis(redisProperties.getLettuce().getPool().getMaxWait().getSeconds());

        // 支持自适应集群拓扑刷新和动态刷新源
        ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
            .enableAllAdaptiveRefreshTriggers()
            // 开启自适应刷新
            .enableAdaptiveRefreshTrigger()
            // 开启定时刷新
            .enablePeriodicRefresh(Duration.ofSeconds(5))
            .build();

        ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
            .topologyRefreshOptions(clusterTopologyRefreshOptions).build();

        LettuceClientConfiguration lettuceClientConfiguration = LettucePoolingClientConfiguration.builder()
            .poolConfig(genericObjectPoolConfig)	//如果使用默认配置可以注释genericObjectPoolConfig
//            .readFrom(ReadFrom.SLAVE_PREFERRED)  //读写分离:主写从读模式配置
            .clientOptions(clusterClientOptions).build();

        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisClusterConfiguration, lettuceClientConfiguration);
        lettuceConnectionFactory.setShareNativeConnection(false);// 是否允许多个线程操作共用同一个缓存连接,默认 true,false 时每个操作都将开辟新的连接
        lettuceConnectionFactory.resetConnection();// 重置底层共享连接, 在接下来的访问时初始化
        return lettuceConnectionFactory;
    }

    /**
     * RedisTemplate配置
     */
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(LettuceConnectionFactory factory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);

        // 使用注解@Bean返回RedisTemplate的时候,同时配置hashkey和hashValue的序列虎方式
        // key采用String的序列化方式
        redisTemplate.setKeySerializer(keySerializer());
        // value使用jackson序列化方式
        redisTemplate.setValueSerializer(valueSerializer());

        // hash的key采用String的序列化方式
        redisTemplate.setHashKeySerializer(keySerializer());
        // hash的value使用jackson序列化方式
        redisTemplate.setHashValueSerializer(valueSerializer());

        /**必须执行这个函数,初始化RedisTemplate*/
        // 需要先调用afterPropertiesSet方法,此方法是应该是初始化参数和初始化工作。
        redisTemplate.afterPropertiesSet();
        log.info("序列化完成!");
        return redisTemplate;
    }




    @Bean
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuffer sb = new StringBuffer();
                sb.append(target.getClass().getName());
                sb.append(method.getName());
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
    }



    /**
     * key键序列化方式
     *
     * @return RedisSerializer
     */
    private RedisSerializer<String> keySerializer() {
        return new StringRedisSerializer();
    }


    /**
     * value值序列化方式
     *
     * @return
     */
    private Jackson2JsonRedisSerializer valueSerializer() {
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        return jackson2JsonRedisSerializer;
    }
}

配置文件:

#Redis Configuration
spring.redis.cluster.max-redirects=10
spring.redis.cluster.nodes=127.0.0.1:8001,127.0.0.1:8002
spring.redis.timeout=60000ms
spring.redis.password=
spring.redis.lettuce.pool.max-active=10
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.pool.max-wait=-1ms

 注意:注入LettuceConnectionFactory后,一定要记得注入RedisTemplate,并 redisTemplate.setConnectionFactory(factory);

apache commons-pool2 包提供了一个通用的对象池技术的实现。可以很方便的基于它来实现自己的对象池,比如 DBCP 和 Jedis 他们的内部对象池的实现就是依赖于 commons-pool2 。

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.8.0</version>
</dependency>

第三种情况:springboot2.3之后版本环境

springboot2.3之后版本默认使用lettuce,默认支持属性配置开启集群拓扑刷新,其解决方案:属性配置开启即可。

spring.redis.lettuce.cluster.refresh.adaptive= true
spring.redis.lettuce.cluster.refresh.period=30000    # 30秒自动刷新一次

 

关联文章:Spring Boot集成Redis集群报错UnsupportedOperationException-CSDN博客

Redis集群拓扑动态刷新是指在Redis集群中,当节点的拓扑结构发生变化时,如新增或删除节点,需要对集群进行重新配置和刷新。这样可以确保集群的高可用性和数据一致性。 在Redis集群中,使用了Gossip协议来实现节点之间的信息传播和拓扑结构的动态刷新。当一个节点发现有新的节点加入或者有节点离开时,它会将这个信息通过Gossip协议广播给其他节点,其他节点也会相应地更新自己的拓扑结构。 具体的拓扑动态刷新过程如下: 1. 新增节点:当有新的节点加入Redis集群时,它会向集群中的任意一个已知节点发送加入请求。已知节点会将这个请求广播给其他节点,其他节点会将新节点添加到自己的拓扑结构中,并进行数据迁移以保证数据的均衡分布。 2. 删除节点:当有节点离开Redis集群时,它会向集群中的任意一个已知节点发送离开请求。已知节点会将这个请求广播给其他节点,其他节点会将离开的节点从自己的拓扑结构中移除,并进行数据迁移以保证数据的均衡分布。 3. 拓扑结构刷新:当有节点加入或离开Redis集群时,所有的节点都会更新自己的拓扑结构,并将最新的拓扑结构信息广播给其他节点。这样每个节点都能够及时了解到集群的最新状态。 通过以上的拓扑动态刷新机制,Redis集群能够实现高可用性和数据一致性。当有节点加入或离开时,集群会自动进行数据迁移和拓扑结构的更新,保证数据的可靠性和高效性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

涛哥是个大帅比

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值