Spring Boot中的Redis保活方案
问题背景
最近,小明在本地开发Spring Boot应用时,发现了一个奇怪的现象。如果他5分钟内没有进行Redis请求,再次请求时,控制台中会出现如下报错:
java.io.IOException: Operation timed out
at java.base/sun.nio.ch.SocketDispatcher.read0(Native Method)
at java.base/sun.nio.ch.SocketDispatcher.read
at java.base/sun.nio.ch.IOUtil.readIntoNativeBuffer
at java.base/sun.nio.ch.IOUtil.read(IOUtil.java:284)
at java.base/sun.nio.ch.IOUtil.read(IOUtil.java:259)
at java.base/sun.nio.ch.SocketChannelImpl.read
at io.netty.buffer.PooledByteBuf.setBytes
at io.netty.buffer.AbstractByteBuf.writeBytes
at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read
at io.netty.channel.nio.NioEventLoop.processSelectedKey
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized
at io.netty.channel.nio.NioEventLoop.processSelectedKeys
at io.netty.channel.nio.NioEventLoop.run
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run
at io.netty.util.internal.ThreadExecutorMap$2.run
at io.netty.util.concurrent.FastThreadLocalRunnable.run
at java.base/java.lang.Thread.run(Thread.java:840)
项目中使用了 Lettuce
来作为Redis客户端库。Redis是安装在远程服务器中的,在本地开发时,Redis客户端需要通过网络连接
来访问Redis。
奇怪的是,一旦出错后,Redis 就又能正常访问了。
如果他在5分钟内频繁对Redis进行请求,也不会出现该报错。
另外,如果将Spring Boot应用直接打包,放到和Redis同一台服务器中(走localhost连接,不通过远程网络访问)时也不会存在该问题。只有本地开发时才有该现象。
原因分析
小明是个爱思考的学生,针对这个错误和现象,他查询了相关资料,初步得出了判断:这很可能由于Redis连接空闲超时导致的
。
首先,他通过 wireShark
抓包,得到如下数据:
从图中可以清晰看出,序号208~3589是同一个TCP连接,序号218的那次GET命令都执行成功了,当时时间为32秒,但是在369秒时再次发起GET命令(序号3484),Redis服务端居然就不响应了,出现了大量TCP Retransmission。
最终,客户端主动发送 RST(Reset)
标志的报文,强制关闭了该TCP连接(序号3589)。
而从序号3590开始,客户端就又创建了一个新TCP连接,进行了TCP三次握手,握手成功后,Redis客户端开始使用约定的用户名和密码进行登录请求。
现象已经很清楚了,那为啥之前的那个TCP连接就无法再连接到服务端呢?
小明仔细想了想,有如下两种可能:
- Redis服务端主动关闭了空闲连接
- 中间设备(防火墙/NAT/LB)丢弃了连接
他查询了服务器上的 redis.conf
文件,其中 timeout
的值为0,tcp-keepalive
的值为300。这两个值z证明不是Redis服务器主动关闭了空闲连接。为了证实想法,他在自己电脑上继续使用 wireShark 抓包,同时,在服务器上使用 tcpdump
抓包:
tcpdump -i eth0 -nn port 6379
5分钟后,在 tcpdump 信息中没有出现 FIN 或 RST 标志的报文。这就是说,双方都没有收到对方的 RST
的信息。
到这里为止,也就明确了原因,就是中间设备丢弃了空闲连接
,导致通信双方互不知情。
解决方案
因为中间设备是不可控的,况且也不知道到底是哪台中间设备的问题。
既然这样,那么就手动增加一个保活方案。创建一个定时任务,客户端每间隔2分钟主动向远程服务进行一次ping:
@Component
public class RedisKeepAliveTask {
private final StringRedisTemplate redisTemplate;
public RedisKeepAliveTask(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Scheduled(fixedRate = 120000) // 每2分钟执行一次
public void keepAlive() {
try {
Objects.requireNonNull(redisTemplate.getConnectionFactory())
.getConnection()
.ping();
} catch (Exception e) {
System.err.println("Redis KeepAlive 失败:" + e.getMessage());
}
}
}
然后,通过 wireShark 抓包,得到如下信息:
可以看到,每间隔2分钟,Spring Boot应用都会向远程 Redis 服务器主动发出一次 PING ,Redis服务器收到后,回复 PONG。
通过这种方式,主动将TCP连接进行了保活,防止了本地开发时Redis的TCP连接失效的问题。