現象:
Springboot 接入redis后發(fā)現隔一段時間連接會超時 command timed out。
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)
at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41)
at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44)
at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42)
at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:273)
at org.springframework.data.redis.connection.lettuce.LettuceStringCommands.convertLettuceAccessException(LettuceStringCommands.java:799)
at org.springframework.data.redis.connection.lettuce.LettuceStringCommands.get(LettuceStringCommands.java:68)
at org.springframework.data.redis.connection.DefaultedRedisConnection.get(DefaultedRedisConnection.java:266)
at org.springframework.data.redis.core.DefaultValueOperations$1.inRedis(DefaultValueOperations.java:57)
at org.springframework.data.redis.core.AbstractOperations$ValueDeserializingRedisCallback.doInRedis(AbstractOperations.java:60)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:228)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:188)
at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:96)
at org.springframework.data.redis.core.DefaultValueOperations.get(DefaultValueOperations.java:53)
原因:
spring-data-redis內置了兩款驅動,jedis和lettuce。springboot1.X版本默認jedis實現,springboot2.X默認lettuce實現。
lettuce:基于netty實現,線程安全,但默認只有一個實例。
jedis:直連redis服務端,一般配合連接池使用,可以增加物理連接。
lettuce只要斷掉連接后,自己并沒有去重連的機制,最后導致time out。
解決辦法:
方法一:
更改連接redis的連接方式,使用jedis連接
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!-- 過濾lettuce,使用jedis作為redis客戶端 -->
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.Apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
配置文件
spring:
redis:
password: xxx
host: 172.16.0.x
port: 6579
timeout: 5000
jedis:
pool:
#最大連接數據庫連接數,設 0 為沒有限制
max-active: 8
#最大等待連接中的數量,設 0 為沒有限制
max-idle: 8
#最大建立連接等待時間。如果超過此時間將接到異常。設為-1表示無限制。
max-wait: -1ms
#最小等待連接中的數量,設 0 為沒有限制
min-idle: 0
#lettuce:
#pool:
#max-active: ${redis.config.maxTotal:1024}
#max-idle: ${redis.config.maxIdle:50}
#min-idle: ${redis.config.minIdle:1}
#max-wait: ${redis.config.maxWaitMillis:5000}
方法二:
在開發(fā)的時候,使用到Lettuce連接redis,一段時間后不操作,再去操作redis,會報連接超時錯誤,在其重連后又可使用。
原因是:Lettuce 自適應拓撲刷新(Adaptive updates)與定時拓撲刷新(Periodic updates) 是默認關閉的導致問題的出現
1、重寫連接工廠實例,更改其
LettuceClientConfiguration 為開啟拓撲更新
@Configuration
public class RedisConfig {
@Autowired
private RedisProperties redisProperties;
//這是固定的模板
//自己定義了一個RedisTemplate
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(@Qualifier("lettuceConnectionFactoryUvPv") RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
//Json序列化配置
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMApper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(om.getPolymorphicTypeValidator());
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
//解決序列化問題
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
jackson2JsonRedisSerializer.setObjectMapper(om);
//String的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
//hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
//value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
//hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
/**
* 為RedisTemplate配置Redis連接工廠實現
* LettuceConnectionFactory實現了RedisConnectionFactory接口
* UVPV用Redis
*
* @return 返回LettuceConnectionFactory
*/
@Bean(destroyMethod = "destroy")
//這里要注意的是,在構建LettuceConnectionFactory 時,如果不使用內置的destroyMethod,可能會導致Redis連接早于其它Bean被銷毀
public LettuceConnectionFactory lettuceConnectionFactoryUvPv() throws Exception {
// List<String> clusterNodes = redisProperties.getCluster().getNodes();
// Set<RedisNode> nodes = new HashSet<>();
// clusterNodes.forEach(address -> nodes.add(new RedisNode(address.split(":")[0].trim(), Integer.parseInt(address.split(":")[1]))));
// RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration();
// clusterConfiguration.setClusterNodes(nodes);
// clusterConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword()));
// clusterConfiguration.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());
//我使用的是單機redis,集群使用上面注釋的代碼
Set<RedisNode> nodes = new HashSet<>();
nodes.add(new RedisNode(redisProperties.getHost(), redisProperties.getPort()));
RedisStandaloneConfiguration redisStandaloneConfiguration=new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(redisProperties.getHost());
redisStandaloneConfiguration.setPassword(redisProperties.getPassword());
redisStandaloneConfiguration.setDatabase(redisProperties.getDatabase());
redisStandaloneConfiguration.setPort(redisProperties.getPort());
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
poolConfig.setMaxIdle(redisProperties.getLettuce().getPool().getMaxIdle());
poolConfig.setMinIdle(redisProperties.getLettuce().getPool().getMinIdle());
poolConfig.setMaxTotal(redisProperties.getLettuce().getPool().getMaxActive());
return new LettuceConnectionFactory(redisStandaloneConfiguration, getLettuceClientConfiguration(poolConfig));
}
/**
* 配置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)
//開啟所有自適應刷新,MOVED,ASK,PERSISTENT都會觸發(fā)
.enableAllAdaptiveRefreshTriggers()
// 自適應刷新超時時間(默認30秒)
.adaptiveRefreshTriggersTimeout(Duration.ofSeconds(25)) //默認關閉開啟后時間為30秒
// 開周期刷新
.enablePeriodicRefresh(Duration.ofSeconds(20)) // 默認關閉開啟后時間為60秒 ClusterTopologyRefreshOptions.DEFAULT_REFRESH_PERIOD 60 .enablePeriodicRefresh(Duration.ofSeconds(2)) = .enablePeriodicRefresh().refreshPeriod(Duration.ofSeconds(2))
.build();
return LettucePoolingClientConfiguration.builder()
.poolConfig(genericObjectPoolConfig)
.clientOptions(ClusterClientOptions.builder().topologyRefreshOptions(topologyRefreshOptions).build())
//將appID傳入連接,方便Redis監(jiān)控中查看
//.clientName(appName + "_lettuce")
.build();
}
}
2、SpringBoot2.3.x后,可使用配置文件中開啟lettuce的拓撲刷新
lettuce:
pool:
max-active: 20
max-wait: -1ms
max-idle: 10
min-idle: 2
cluster:
refresh:
adaptive: true
#20秒自動刷新一次
period: 20