Spring Boot中的Redis保活方案

阅读 73

问题背景

最近,小明在本地开发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连接失效的问题。

最后编辑于: 2025-03-29

评论(0条)

(必填)
复制成功