上个月线上生产环境有几个接口出现异常响应,查看生产日志后发现,如下错误
线上 Redis 客户端使用的是SpringBoot
默认的Lettuce
客户端,并且没有指定连接池,connection reset by peer
这个错误是当前客户端连接在不知情的情况下被服务端断开后产生,也就是说当前客户端 Redis 连接已经在服务端断开了,但是客户端并不知道,当请求进来时,Lettuce
继续使用当前 Redis 连接请求数据时,就会提示connection reset by peer
。
一般情况下服务端断开连接都会发送FIN
包通知客户端,但是当我在用tcpdump
监控服务端 tcp 传输后,发现 Redis 服务端 tcp 连接在无活动一段时间,比如 10 分钟后会收到来自客户端的RST
包,然而我的客户端也在使用 wireshark 抓包中,并没有发送给服务端RST
包,这就很奇怪了,猜测这里是可能是服务器对 tcp 连接的限制导致,对长时间无活动的 tcp 连接强制断开处理。所以这里线上环境 Redis 连接偶尔产生connection reset by peer
错误是被我复现出来了。
既然这里知道是 Redis 连接长时间无活动后被断开导致的 bug ,那怎么解决?
博主一开始以为重试可以解决,但是发现事情没有想象的简单。上代码
// 查询 Redis
public <T> T getCacheObject(final String key) {
try {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
} catch (Exception e) {
log.error(e.getMessage(), e);
return retryGetCacheObject(key, 3);
}
}
// 重试查询 Redis
public <T> T retryGetCacheObject(final String key, int retryCount) {
try {
log.info("retryGetCacheObject, key:{}, retryCount:{}", key, retryCount);
if (retryCount <= 0) {
return null;
}
Thread.sleep(200L);
retryCount--;
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
} catch (Exception e) {
log.error(e.getMessage(), e);
return retryGetCacheObject(key, retryCount);
}
}
上面代码的意思是第一次查询 Redis 发生异常后,每隔 200 毫秒在查 3 次。当实际运行时,发现这里会提示三次connection reset by peer
错误,一直没有取到新的 Redis 连接。
到这里这个问题的我的解决思路其实就是怎么在 Redis 连接发生异常后,怎么创建一条新的连接进行代替。
不多说直接上代码:
// Lettuce 连接工厂
@Autowired
private LettuceConnectionFactory lettuceConnectionFactory;
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key) {
try {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
} catch (Exception e) {
log.error(e.getMessage(), e);
return retryGetCacheObject(key, 1);
}
}
public <T> T retryGetCacheObject(final String key, int retryCount) {
try {
log.info("retryGetCacheObject, key:{}, retryCount:{}", key, retryCount);
if (retryCount <= 0) {
return null;
}
lettuceConnectionFactory.resetConnection();
Thread.sleep(200L);
retryCount--;
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
} catch (Exception e) {
log.error(e.getMessage(), e);
return retryGetCacheObject(key, retryCount);
}
}
在用当前 Redis 连接获取数据发生异常超过timeout
间隔后,抛出异常,进入重试方法,使用 lettuceConnectionFactory.resetConnection()
方法进行连接重置,创建一条新的连接后,继续获取数据,从而正常响应客户端。lettuceConnectionFactory
对象是对Lettuce
无池化连接的工厂实现,提供了 lettuceConnectionFactory.getConnection(); lettuceConnectionFactory.initConnection(); lettuceConnectionFactory.resetConnection();
等获取、初始化、重置连接的方法
配合springboot
配置timeout
将获取数据的超时时间设置为 2 秒,从而将接口请求耗时也控制在 2 秒左右
redis:
xx: xx
timeout: 2000
到此生产环境这里SpringBoot
项目下Lettuce
客户端无池化连接偶尔断开的 bug 算是解决了
最后贴一下实战项目地址newbeemall,newbee-mall 商城的 mybatis plus 版本 实现了优惠卷领取, 支付宝沙箱支付,后台添加搜索,RedisSearch 分词检索
1
lianjun1991 2022-04-10 22:36:44 +08:00
是不是防火墙的问题,长时间未使用的连接,在下一次使用的时候被防火墙 reset 了
|
2
kchenzhi 2022-04-11 09:58:09 +08:00
一楼讲的不错,我曾经遇到过类似的问题,客户端和服务器都还保持这链接,但是由于过久没有报文,防火墙把端口回收了。可以抓一下规律,一般是固定几分钟后防火墙会释放端口,如果此时在 redis 服务器上用 netstat 查看链接信息仍然是 ESTABLISHED 的话,那说明服务端也还保持着这个连接。
解决方案有两个: 1 、增加防火墙的空闲端口回收时间。但是这样对网络影响较大,不太建议用。 2 、查找 lettuce 文档,看是否有心跳参数,把心跳间隔调整得比防火墙超时时间短。 3 、自己定时发送指令,实现心跳机制。 |
3
leeyuzhe 2022-04-11 13:34:54 +08:00
我之前遇到过类似的问题,应该不是防火墙的原因,因为我这边程序会一直使用 redis ,后来换成 jedis 不了了之
|
4
wayn111 OP @lianjun1991 是由这个可能,在无活动一段时间后,这个 tcp 连接收到了客户端的 rst 包,但是客户都安并没有发包
|