Springboot2.x集成lettuce连接redis集群报超时异常怎么解决

背景:最近在对一新开发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 <

【技术分享】Springboot2.x集成lettuce连接redis集群报超时异常解决方案


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连接失败的时候。



一、问题描述
在使用Springboot2.x集成lettuce连接redis集群的时候,会出现连接超时的问题,导致无法正常操作redis集群,这极大地影响了系统的使用效率和稳定性。
二、问题分析
根据问题描述,可能是因为redis集群中的某一个节点宕掉或网络故障导致连接超时,需要通过对redis集群的监控和调整来解决这一问题。
三、检查redis集群状态
可以通过调用redis集群的cluster nodes命令来检查redis集群的节点状态,如下所示:
cluster nodes
解析结果如下所示:
3275e9f76c7a0f9c39e441865e29d3810cbebfe4 192.168.1.1:6379 master - 0 1582617486720 1 connected 0-5460
64fa73a921f58a7bcbb56e0e2850f954f2da39d2 192.168.1.2:6379 master - 0 1582617485235 0 connected 10923-16383
e9e9b6260d6654204525728f7f1ebc2871e8a5a1 192.168.1.3:6379 master - 0 1582617484745 3 connected 5461-10922
...
根据命令执行结果,可以查看到redis集群中的节点状态以及节点的连接状态。
四、调整redis集群配置
针对连接超时的问题,需要对redis集群的配置进行调整,如下所示:
redis.conf文件中配置:
cluster-announce-ip [该集群的ip地址]
cluster-announce-port [该集群的端口号]
cluster-announce-bus-port [该集群的消息总线端口号]
例如:
cluster-announce-ip 192.168.1.1
cluster-announce-port 6379
cluster-announce-bus-port 16380
其中cluster-announce-bus-port是redis集群内部节点间通信的端口号,需要保证集群中所有节点都开放该端口并且没有被其他服务占用。
五、尝试重启redis集群
如果以上调整仍然不能解决问题,可以尝试重启redis集群,通过重启来清除可能存在的节点故障或者网络故障。
六、配置lettuce超时时间
如果以上方法均无法解决问题,可能就需要在lettuce中配置超时时间,避免因超时时间过长导致java线程阻塞,引起其它的问题。
可以在application.properties中添加如下配置:
# 连接池最大连接数
spring.redis.lettuce.pool.max-active=200
# 连接池最大阻塞等待时间,-1表示没有限制
spring.redis.lettuce.pool.max-wait=-1ms
# 连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=50
# 连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=10
spring.redis.lettuce.shutdown-timeout=100ms
# lettuce超时时间,单位:ms
spring.redis.timeout=3000
其中,spring.redis.timeout为lettuce的超时时间,可以设置合适的数值来适应项目实际情况,同时让连接池尽可能地完成redis请求并释放资源,提高程序的响应速度和稳定性。
七、总结
通过对以上方案的实施,可以有针对性地解决Springboot2.x集成lettuce连接redis集群报超时异常的问题,保证系统的正常运行。同时,这一过程也深化了我们对redis集群、lettuce等技术的理解,对今后的开发和运维工作具有重要意义。