敲黑板!划重点!:自适应拓扑刷新(Adaptive updates)与定时拓扑刷新(Periodic updates)

  • 坑爹的拓扑刷新
  • 查找原因
  • 移植代码


坑爹的拓扑刷新

之前使用公司封装好的Redis组件(内部使用了老版本的Jedis)。后来公司不再维护组件了,各项目自行通过开源Redis组件连接。
我这边直接选择了使用Spring Data Redis+Lettuce来搭建Redis客户端。上依赖关系:

<dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>2.1.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
            <version>5.1.4.RELEASE</version>
        </dependency>

参照了另一篇博客的文章内容《SpringDataRedis整合Lettuce学习整理》,开心地搭好了之后,突然有一天线上各种报错:

org.springframework.dao.QueryTimeoutException: Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException: Command timed out after 5 second(s)
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:70) ~[spring-data-redis-2.1.5.RELEASE.jar!/:2.1.5.RELEASE]
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41) ~[spring-data-redis-2.1.5.RELEASE.jar!/:2.1.5.RELEASE]
    at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44) ~[spring-data-redis-2.1.5.RELEASE.jar!/:2.1.5.RELEASE]
    at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42) ~[spring-data-redis-2.1.5.RELEASE.jar!/:2.1.5.RELEASE]
    at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:268) ~[spring-data-redis-2.1.5.RELEASE.jar!/:2.1.5.RELEASE]
    at org.springframework.data.redis.connection.lettuce.LettuceKeyCommands.convertLettuceAccessException(LettuceKeyCommands.java:817) ~[spring-data-redis-2.1.5.RELEASE.jar!/:2.1.5.RELEASE]
    at org.springframework.data.redis.connection.lettuce.LettuceKeyCommands.exists(LettuceKeyCommands.java:80) ~[spring-data-redis-2.1.5.RELEASE.jar!/:2.1.5.RELEASE]
    at org.springframework.data.redis.connection.DefaultedRedisConnection.exists(DefaultedRedisConnection.java:55) ~[spring-data-redis-2.1.5.RELEASE.jar!/:2.1.5.RELEASE]
    at org.springframework.data.redis.connection.DefaultStringRedisConnection.exists(DefaultStringRedisConnection.java:314) ~[spring-data-redis-2.1.5.RELEASE.jar!/:2.1.5.RELEASE]
    at org.springframework.data.redis.core.RedisTemplate.lambda$hasKey$6(RedisTemplate.java:769) ~[spring-data-redis-2.1.5.RELEASE.jar!/:2.1.5.RELEASE]
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:224) ~[spring-data-redis-2.1.5.RELEASE.jar!/:2.1.5.RELEASE]
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:184) ~[spring-data-redis-2.1.5.RELEASE.jar!/:2.1.5.RELEASE]
    at org.springframework.data.redis.core.RedisTemplate.hasKey(RedisTemplate.java:769) ~[spring-data-redis-2.1.5.RELEASE.jar!/:2.1.5.RELEASE]

排查原因后发现是使用的Redis集群中有一台master节点异常,被替换了。
(说明:我们线上用的Redis是Cluster)

奇怪! 这时候不是应该会自动切换到新的节点的么?

查找原因

经过各种求证以后,找到原因是:节点异常,服务端的Redis集群拓扑被刷新了,但是不止为何,本地没有获取到新的拓扑。
再次各种搜索之后,在Lettuce官方文档中找到了关于Redis Cluster的相关信息《Redis Cluster

这里面的大概意思是 自适应拓扑刷新(Adaptive updates)与定时拓扑刷新(Periodic updates) 是默认关闭的,可以通过如下代码打开。

Example 4. Enabling periodic cluster topology view updates

RedisClusterClient clusterClient = RedisClusterClient.create(RedisURI.create("localhost", 6379));

ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
                                .enablePeriodicRefresh(10, TimeUnit.MINUTES)
                                .build();

clusterClient.setOptions(ClusterClientOptions.builder()
                                .topologyRefreshOptions(topologyRefreshOptions)
                                .build());
...

clusterClient.shutdown();

Example 5. Enabling adaptive cluster topology view updates

RedisURI node1 = RedisURI.create("node1", 6379);
RedisURI node2 = RedisURI.create("node2", 6379);

RedisClusterClient clusterClient = RedisClusterClient.create(Arrays.asList(node1, node2));

ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
                                .enableAdaptiveRefreshTrigger(RefreshTrigger.MOVED_REDIRECT, RefreshTrigger.PERSISTENT_RECONNECTS)
                                .adaptiveRefreshTriggersTimeout(30, TimeUnit.SECONDS)
                                .build();

clusterClient.setOptions(ClusterClientOptions.builder()
                                .topologyRefreshOptions(topologyRefreshOptions)
                                .build());
...

clusterClient.shutdown();

这两段代码的核心,就是要将ClusterTopologyRefreshOptions通过ClusterTopologyRefreshOptions.builder()的方式构建后,塞入RedisClusterClient中。

问题来了:通过Sprint Redis Data构建Redis时,没有显式构建RedisClusterClient。

移植代码

分析源码后发现RedisClusterClient是在构建LettuceConnectionFactory时构建的,这时依赖了LettuceConnectionFactory构造方法中的传参clientConfig

public LettuceConnectionFactory(RedisClusterConfiguration clusterConfiguration, LettuceClientConfiguration clientConfig)

再经过查找,上方文档中使用的ClusterClientOptions.builder().build()构建出的实体是LettucePoolingClientConfiguration.builder().clientOptions()方法的入参。所以对应的配置是在这条语句中注入的。
上具体代码了

package com.ly.train.sb.config;

import com.ly.train.sb.common.utils.GetRedisConfig;
import com.ly.train.sb.common.utils.RedisStringUtil;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
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.RedisConnectionFactory;
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.StringRedisTemplate;

import java.time.Duration;

/**
 * RedisConfig
 * 最终目标为:StringRedisTemplate或者RedisTemplate
 * <p>
 * 需要 : RedisConnectionFactory
 * <p>
 * 其实现为 : LettuceConnectionFactory
 * <p>
 * 因实现sentinel,所以需要构造参数:
 * <p>
 * 1、RedisClusterConfiguration,其配置sentinel-master和ip:host
 * 2、LettuceClientConfiguration,可其配置连接池以及ssl等相关参数(使用其子接口LettucePoolingClientConfiguration)
 * <p>
 * LettucePoolingClientConfiguration.可通过static方法使用builder模式创建:
 * <p>
 * LettucePoolingClientConfiguration.builder().poolConfig(pool).build();
 * 1
 * pool类型GenericObjectPoolConfig为common-pool2线程池
 *
 * @author John Chen
 * @date 2018/10/30
 */
@Configuration
public class RedisConfig {
    @Value("${tms.project.id}")
    private String appName;

    //region UVPV用Redis构建

    /**
     * UVPVRedis工具类构建
     *
     * @param stringRedisTemplateUvPv StringRedisTemplate实现
     * @return 返回工具类实例
     */
    @Bean
    public RedisStringUtil redisStringUtilUvPv(StringRedisTemplate stringRedisTemplateUvPv) {
        return new RedisStringUtil(stringRedisTemplateUvPv);
    }

    /**
     * 配置StringRedisTemplate
     * 【Redis配置最终一步】
     *
     * @param lettuceConnectionFactoryUvPv redis连接工厂实现
     * @return 返回一个可以使用的StringRedisTemplate实例
     */
    @Bean
    public StringRedisTemplate stringRedisTemplateUvPv(@Qualifier("lettuceConnectionFactoryUvPv") RedisConnectionFactory lettuceConnectionFactoryUvPv) {
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setConnectionFactory(lettuceConnectionFactoryUvPv);
        return stringRedisTemplate;
    }


    /**
     * 为RedisTemplate配置Redis连接工厂实现
     * LettuceConnectionFactory实现了RedisConnectionFactory接口
     * UVPV用Redis
     *
     * @return 返回LettuceConnectionFactory
     */
    @Bean(destroyMethod = "destroy") 
    //这里要注意的是,在构建LettuceConnectionFactory 时,如果不使用内置的destroyMethod,可能会导致Redis连接早于其它Bean被销毁
    public LettuceConnectionFactory lettuceConnectionFactoryUvPv() throws Exception {
    	//公司内部通过Redis名称走组件获取到Redis的具体配置
        String redisName = "xxxxxx";
        return new LettuceConnectionFactory(redisClusterConfiguration(redisName), getLettuceClientConfiguration(genericObjectPoolConfig(20, 10, 100)));
    }
    //endregion


    //region 基础配置

    /**
     * 通过Redis名称获取连接配置
     *
     * @param redisName Redis名称。在申请Redis时填入的
     * @return 返回Redis集群的具体配置
     */
    private RedisClusterConfiguration redisClusterConfiguration(String redisName) throws Exception {
        return GetRedisConfig.getRedisClusterConfiguration(redisName);
    }

    /**
     * 通过Redis名称获取连接配置
     *
     * @param redisName Redis名称。在申请Redis时填入的
     * @return 返回Redis集群的具体配置
     */
    private RedisClusterConfiguration redisClusterConfiguration(String redisName, int timeOutMS) throws Exception {
        return GetRedisConfig.getRedisClusterConfiguration(redisName, timeOutMS);
    }

    /**
     * 配置LettuceClientConfiguration 包括线程池配置和安全项配置
     *
     * @param genericObjectPoolConfig common-pool2线程池
     * @return lettuceClientConfiguration
     */
    private LettuceClientConfiguration getLettuceClientConfiguration(GenericObjectPoolConfig genericObjectPoolConfig) {
        /*
        【重要!!】
        【重要!!】
        【重要!!】
        ClusterTopologyRefreshOptions配置用于开启自适应刷新和定时刷新。如自适应刷新不开启,Redis集群变更时将会导致连接异常!
         */
        ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
                //开启自适应刷新
                .enableAdaptiveRefreshTrigger(ClusterTopologyRefreshOptions.RefreshTrigger.MOVED_REDIRECT, ClusterTopologyRefreshOptions.RefreshTrigger.PERSISTENT_RECONNECTS)
                .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(10))
                //开启定时刷新
                .enablePeriodicRefresh(Duration.ofSeconds(15))
                .build();
        return LettucePoolingClientConfiguration.builder()
                .poolConfig(genericObjectPoolConfig)
                .clientOptions(ClusterClientOptions.builder().topologyRefreshOptions(topologyRefreshOptions).build())
                //将appID传入连接,方便Redis监控中查看
                .clientName(appName + "_lettuce")
      			.build();
    }

    /**
     * 构建Redis连接池
     *
     * @param maxIdle  最大空闲连接数 推荐20
     * @param mixIdle  最小空闲连接数 推荐10
     * @param maxTotal 设置最大连接数,(根据并发请求合理设置)推荐100。
     * @return 返回一个连接池
     */
    private GenericObjectPoolConfig genericObjectPoolConfig(int maxIdle, int mixIdle, int maxTotal) {
        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        poolConfig.setMaxIdle(maxIdle);
        poolConfig.setMinIdle(mixIdle);
        poolConfig.setMaxTotal(maxTotal);
        //此处可添加其它配置
        return poolConfig;
    }
    //endregion


}