zoukankan      html  css  js  c++  java
  • Springboot2.x集成lettuce连接redis集群报超时异常Command timed out after 6 second(s)

    文/朱季谦

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

      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的值设置到更大后,依然没有解决该超时问题。

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

      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的依赖:

     

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

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

    打印输出——

    测试打印驱动类型: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类:

      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的过程当中,会出现连接断开的情况,导致无法新增成功,超过一定时间还没有正常,就会出现连接超时的情况。

    至于是什么原因导致的断开连接,暂时还没有比较好思路,暂且把这个问题留着,等慢慢研究看是否能找到问题所在,若有大神指点,也感激不尽。

    最后发现,阿里云官网上关于redis的客户端连接,是不推荐使用Lettuce客户端——

    作者:朱季谦
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文链接,否则保留追究法律责任的权利。
  • 相关阅读:
    Qt5官方demo解析集21——Extending QML
    多封装,少开放。强烈建议C++标准添加class之间的注入机制
    iOS 设计模式之工厂模式
    golang的select典型用法
    Go的异常处理 defer, panic, recover
    Visual Studio Code 的简单试用体验
    在Visual Studio Code中配置GO开发环境
    Go语言开发环境配置
    Go 语言 很牛
    Go将统治下一个10年?Go语言发展现状分析
  • 原文地址:https://www.cnblogs.com/zhujiqian/p/14552873.html
Copyright © 2011-2022 走看看