Springboot2.x集成lettuce链接redis集群报超时异常Command timed out after 6 second(s)

文/朱季谦java

背景:最近在对一新开发Springboot系统作压测,发现刚开始压测时,能够正常对redis集群进行数据存取,可是暂停几分钟后,接着继续用jmeter进行压测时,发现redis就开始忽然疯狂爆出异常提示:Command timed out after 6 second(s)......node

 1 Caused by: io.lettuce.core.RedisCommandTimeoutException: Command timed out after 6 second(s)
 2     at io.lettuce.core.ExceptionFactory.createTimeoutException(ExceptionFactory.java:51)
 3     at io.lettuce.core.LettuceFutures.awaitOrCancel(LettuceFutures.java:114)
 4     at io.lettuce.core.cluster.ClusterFutureSyncInvocationHandler.handleInvocation(ClusterFutureSyncInvocationHandler.java:123)
 5     at io.lettuce.core.internal.AbstractInvocationHandler.invoke(AbstractInvocationHandler.java:80)
 6     at com.sun.proxy.$Proxy134.mget(Unknown Source)
 7     at org.springframework.data.redis.connection.lettuce.LettuceStringCommands.mGet(LettuceStringCommands.java:119)
 8     ... 15 common frames omitted

我急忙检查redis集群,发现集群里的各节点都一切正常,且cpu和内存使用率还不到百分之二十,看着这一切,我忽然陷入漫长的沉思,究竟是哪里出现问题......百度一番,发现很多人都出现过相似状况的,有人说把超时timeout设置更大一些就能够解决了。我按照这样的解决方法,把超时timeout的值设置到更大后,依然没有解决该超时问题。redis

其中,springboot操做redis的依赖包是——spring

 1 <dependency>
 2     <groupId>org.springframework.boot</groupId>
 3     <artifactId>spring-boot-starter-data-redis</artifactId>
 4 </dependency>

集群配置——安全

 1 redis:
 2   timeout: 6000ms
 3   cluster:
 4     nodes:
 5       - xxx.xxx.x.xxx:6379
 6       - xxx.xxx.x.xxx:6379
 7       - xxx.xxx.x.xxx:6379
 8   jedis:
 9     pool:
 10       max-active: 1000
 11       max-idle: 10
 12       min-idle: 5
 13       max-wait: -1

点进spring-boot-starter-data-redis进去,发现里面包含了lettuce的依赖:springboot

 

看到一些网友说,springboot1.x默认使用的是jedis,到了Springboot2.x就默认使用了lettuce。咱们能够简单验证一下,在redis驱动加载配置类里,输出一下RedisConnectionFactory信息:ide

 1 @Configuration
 2 @AutoConfigureAfter(RedisAutoConfiguration.class)
 3 public class Configuration {
 4     @Bean
 5     public StringRedisTemplate redisTemplate(RedisConnectionFactory factory) {
 6         log.info("测试打印驱动类型:"+factory);
 7 }

打印输出——spring-boot

测试打印驱动类型:org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory@74ee761e

可见,这里使用正是是lettuce驱动链接,所以,当把它换成之前用的比较多的jedis驱动链接时,就没有再出现这个Command timed out after 6 second(s)问题了。测试

 1 <dependency>
 2     <groupId>org.springframework.boot</groupId>
 3     <artifactId>spring-boot-starter-data-redis</artifactId>
 4     <exclusions>
 5         <exclusion>
 6             <groupId>io.lettuce</groupId>
 7             <artifactId>lettuce-core</artifactId>
 8         </exclusion>
 9     </exclusions>
 10 </dependency>
 11 <dependency>
 12     <groupId>redis.clients</groupId>
 13     <artifactId>jedis</artifactId>
 14 </dependency>

那么问题来了,Springboot2.x是如何默认使用了lettuce,这得去研究下里面的部分代码。咱们能够可进入到Springboot2.x自动装配模块的redis部分,其中有一个RedisAutoConfiguration类,其主要做用是对Springboot自动配置链接redis类:ui

 1 @Configuration(
 2     proxyBeanMethods = false
 3 )
 4 @ConditionalOnClass({RedisOperations.class})
 5 @EnableConfigurationProperties({RedisProperties.class})
 6 @Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
 7 public class RedisAutoConfiguration {
 8     public RedisAutoConfiguration() {
 9    }
 10    ......省略
 11 }

这里只须要关注里面的一行注解:

 1 
 2 @Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
 3 

这就意味着使用spring-boot-starter-data-redis依赖时,可自动导入lettuce和jedis两种驱动,按理来讲,不会同时存在两种驱动,这样没有太大意义,所以,这里的前后顺序就很重要了,为何这么说呢?

分别进入到LettuceConnectionConfiguration.class与JedisConnectionConfiguration.class当中,各自展现本文须要涉及到的核心代码:

 1 //LettuceConnectionConfiguration
 2 @ConditionalOnClass({RedisClient.class})
 3 class LettuceConnectionConfiguration extends RedisConnectionConfiguration {
 4    ......省略
 5     @Bean
 6     @ConditionalOnMissingBean({RedisConnectionFactory.class})
 7     LettuceConnectionFactory redisConnectionFactory(ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers, ClientResources clientResources) throws UnknownHostException {
 8         LettuceClientConfiguration clientConfig = this.getLettuceClientConfiguration(builderCustomizers, clientResources, this.getProperties().getLettuce().getPool());
 9         return this.createLettuceConnectionFactory(clientConfig);
 10    }
 11 }
 12 //JedisConnectionConfiguration
 13 @ConditionalOnClass({GenericObjectPool.class, JedisConnection.class, Jedis.class})
 14 class JedisConnectionConfiguration extends RedisConnectionConfiguration {
 15    ......省略
 16     @Bean
 17     @ConditionalOnMissingBean({RedisConnectionFactory.class})
 18     JedisConnectionFactory redisConnectionFactory(ObjectProvider<JedisClientConfigurationBuilderCustomizer> builderCustomizers) throws UnknownHostException {
 19         return this.createJedisConnectionFactory(builderCustomizers);
 20    }
 21 }
 22 

可见,LettuceConnectionConfiguration.class与JedisConnectionConfiguration.class当中都有一个相同的注解 @ConditionalOnMissingBean({RedisConnectionFactory.class}),这是说,假如RedisConnectionFactory这个bean已经被注册到容器里,那么与它类似的其余Bean就不会再被加载注册,简单点说,对LettuceConnectionConfiguration与JedisConnectionConfiguration各自加上 @ConditionalOnMissingBean({RedisConnectionFactory.class})注解,二者当中只能加载注册其中一个到容器里,另一个就不会再进行加载注册。

那么,问题就来了,谁会先被注册呢?

这就回到了上面提到的一句,@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})这一句里的前后顺序很关键,LettuceConnectionConfiguration在前面,就意味着,LettuceConnectionConfiguration将会被注册。

可见,Springboot默认是使用lettuce来链接redis的。

当咱们引入spring-boot-starter-data-redis依赖包时,其实就至关于引入lettuce包,这时就会使用lettuce驱动,若不想使用该默认的lettuce驱动,直接将lettuce依赖排除便可。

 1 <dependency>
 2     <groupId>org.springframework.boot</groupId>
 3     <artifactId>spring-boot-starter-data-redis</artifactId>
 4     <exclusions>
 5         <exclusion>
 6             <groupId>io.lettuce</groupId>
 7             <artifactId>lettuce-core</artifactId>
 8         </exclusion>
 9     </exclusions>
 10 </dependency>

而后再引入jedis依赖——

 1 <dependency>
 2     <groupId>redis.clients</groupId>
 3     <artifactId>jedis</artifactId>
 4 </dependency>

这样,在进行RedisAutoConfiguration的导入注解时,由于没有找到lettuce依赖,故而这注解@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})的第二个位置上的JedisConnectionConfiguration就有效了,就能够被注册到容器了,当作springboot操做redis的驱动。

lettuce与jedis二者有什么区别呢?

lettuce:底层是用netty实现,线程安全,默认只有一个实例。

jedis:可直连redis服务端,配合链接池使用,可增长物理链接。

根据异常提示找到出现错误的方法,在下列代码里的LettuceConverters.toBoolean(this.getConnection().zadd(key, score, value))——

 1 public Boolean zAdd(byte[] key, double score, byte[] value) {
 2     Assert.notNull(key, "Key must not be null!");
 3     Assert.notNull(value, "Value must not be null!");
 4 5     try {
 6         if (this.isPipelined()) {
 7             this.pipeline(this.connection.newLettuceResult(this.getAsyncConnection().zadd(key, score, value), LettuceConverters.longToBoolean()));
 8             return null;
 9        } else if (this.isQueueing()) {
 10             this.transaction(this.connection.newLettuceResult(this.getAsyncConnection().zadd(key, score, value), LettuceConverters.longToBoolean()));
 11             return null;
 12        } else {
 13             return LettuceConverters.toBoolean(this.getConnection().zadd(key, score, value));
 14        }
 15    } catch (Exception var6) {
 16         throw this.convertLettuceAccessException(var6);
 17    }
 18 }

LettuceConverters.toBoolean()是将long转为Boolean,正常状况下,this.getConnection().zadd(key, score, value)若是新增成功话,那么返回1,这样LettuceConverters.toBoolean(1)获得的是true,反之,若是新增失败,则返回0,即LettuceConverters.toBoolean(0),还有第三种状况,就是这个this.getConnection().zadd(key, score, value)方法出现异常,什么状况下会出现异常呢?

应该是,connection链接失败的时候。

这就意味着,以lettuce驱动链接redis的过程中,会出现链接断开的状况,致使没法新增成功,超过必定时间尚未正常,就会出现链接超时的状况。

至因而什么缘由致使的断开链接,暂时尚未比较好思路,暂且把这个问题留着,等慢慢研究看是否能找到问题所在,如有大神指点,也感激涕零。

相关文章
相关标签/搜索