解决springboot中链接redis会timeout的问题
之前写项目遇见一个问题,就是我的项目上线后,总是经常会报错timeout,我一看是redis链接timeout,首先我端口打开了,为啥还报错我就很纳闷,只得先重启一下防火墙。重启过后的确可以正常访问了,但是一段时间后依然还是会出现timeout的错误。只能在重启,这个问题始终没有得到根本性的解决!
因为我使用的是springboot,所以一开始并没有往代码方向去考虑,直接怀疑服务器有问题,(比较springboot目前用的人比较多也没出啥问题也就比较相信它,同时我服务器又是最便宜的哪一款,就怀疑到服务器头上了,便宜没好货哈哈),但还是没有发现有啥问题,最后只能去看代码...
后面我用jedis重新写了个小test,发现正常链接,但是springboot的redisTemplate就是有问题,那原因清楚了,可能真的是springboot的问题~
我看了springboot内嵌实现链接redis的源码,发现springboot内嵌了Luttcure的技术,Luttuce和jedis pool都是一种池化技术,就和数据库的Durid、HkariaCP等这些链接池一样。
而Luttuce有个问题就是Luttuce不会自动刷新redis的拓扑结构的,所以会造成一段时间过后导致链接超时,毕竟链接池也是有最大等待时间的。
拓扑结构
拓扑结构也称为树状主从架构,咱们平常熟悉的主从架构便是通过读写分离来降低住服务器的压力,但是一般master节点进行写操作,slave节点进行读操作,在读多写少的场景,虽然降低了主服务器的压力,比如执行keys sort等操作保证了master的稳定性,但如果写并发某个时刻比较高时,主节点需要向挂载的多个slave节点发送写命令,这就又可能导致master的负载过高而影响服务的稳定性。
因此我们就产生了树状主从架构,slave节点可以复制主节点数据,也可以作为其他slave节点的master节点继续向下复制。解决了主从的不足
master --> slave1 --> slave2 --> slave3
这降低了主节点的负载,所以如果master需要挂载多个slave时,而我们又想保证master的稳定性,就可以采用这种拓扑结构。
而springboot内嵌的Luttuce链接池,默认不会改变redis集群的拓扑结构,当我们在超时时间触发后,springboot与redis的链接就断开了,而下次访问时还会访问这个已经超时的拓扑节点,导致访问不成功。这就是springboot操作redis偶尔会timeout的问题原因所在。
解决方案呢有很多,大致就是及时更新这个拓扑节点就好了,目前可以通过在Luttuce留下的钩子函数中去自定义刷新的拓扑逻辑,比如启一个cron定时任务去做一个心跳检测去验证链接是否异常来完成。
第一种方案:
springboot中预留的钩子函数中执行的customize的方法。
在springboot预留的接口中自定义类实现LettuceClientConfigurationBuilderCustomizer接口,实现customize方法,其中开启自定义刷新和定时刷新,而springboot实现的钩子函数中会自动去执行customize方法
/**
* 通过ClusterTopologyRefreshOptions 开启定时刷新和自适应刷新
* @author azh
*/
public class MyLuttuceCronTask implements LettuceClientConfigurationBuilderCustomizer {
public void customize(org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder clientConfigurationBuilder) {
// ClusterTopologyRefreshOptions 用于开启自适应刷新和定时刷新 防止timeout 错误
ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
// 开始自适应刷新
.enableAdaptiveRefreshTrigger(ClusterTopologyRefreshOptions.RefreshTrigger.MOVED_REDIRECT, ClusterTopologyRefreshOptions.RefreshTrigger.PERSISTENT_RECONNECTS)
.enableAllAdaptiveRefreshTriggers()
.adaptiveRefreshTriggersTimeout(Duration.ofSeconds(10))
//开启定时刷新 时间间隔自己定义
.enablePeriodicRefresh(Duration.ofSeconds(15))
.build();
clientConfigurationBuilder.clientOptions(ClusterClientOptions.builder().topologyRefreshOptions(topologyRefreshOptions).build());
}
}
第二种方案:
Lettuce提供了链接校验的方法,只是默认没有开启,我们可以开启它,然后通过一个定时任务来解决这个timeout的问题。
/**
* 开启获取链接的校验
* @author azh
*/
public class GetLettuceConnection implements InitializingBean {
private RedisConnectionFactory redisConnectionFactory;
public void afterPropertiesSet() throws Exception {
if (redisConnectionFactory instanceof LettuceConnectionFactory) {
LettuceConnectionFactory factory = (LettuceConnectionFactory) redisConnectionFactory;
factory.setValidateConnection(true);
}
}
}
/**
* 每隔2s校验lettuce是否异常,解决lettuce因为不更新redis拓扑结构导致netty无法及时监控到导致timeout的问题
* @author azh
*/
public class RedisCronTask {
private RedisConnectionFactory redisConnectionFactory;
// 每2s执行一下
"0/2 * * * * ?") (cron =
public void task() {
if (redisConnectionFactory instanceof LettuceConnectionFactory) {
LettuceConnectionFactory factory = (LettuceConnectionFactory) redisConnectionFactory;
factory.validateConnection();
}
}
}
开启获取链接的校验
定时任务去校验luttuce是否异常
最后其实还有个比较简单但头疼的解决方案,那就是每隔一段时间都往redis的某个数据库中去set一个值,不过这种方案会耗费网络资源,但耗费也不是太大,算是一种中肯的解决办法吧。