敲黑板!划重点!:自适应拓扑刷新(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
}