Letture闲置一段时间后连接失败

Lettuce闲置一段时间后连接失败 #

问题描述 #

环境:

  • java 8
  • springboot 2.5.5 (其中 redis 客户端默认使用 lettuce 6.1.5)
  • redis 5.0 (官方 docker 镜像,默认配置)

应用配置:

yml
spring:
  redis:
    host: xxx
    port: xxx
    password: xxx
    database: xxx

现象:

使用 redis 连接之后闲置一段时间(大概20分钟),再次连接时报错:远程主机强迫关闭了一个现有的连接。

text
2024-01-16 22:43:24.429 ERROR 15328 --- [nio-8082-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception

org.springframework.data.redis.RedisSystemException: Redis exception; nested exception is io.lettuce.core.RedisException: java.io.IOException: 远程主机强迫关闭了一个现有的连接。
	at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:74) ~[spring-data-redis-2.5.5.jar:2.5.5]
	at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41) ~[spring-data-redis-2.5.5.jar:2.5.5]
	at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44) ~[spring-data-redis-2.5.5.jar:2.5.5]
	at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42) ~[spring-data-redis-2.5.5.jar:2.5.5]
	at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:271) ~[spring-data-redis-2.5.5.jar:2.5.5]
	at org.springframework.data.redis.connection.lettuce.LettuceConnection.await(LettuceConnection.java:1062) ~[spring-data-redis-2.5.5.jar:2.5.5]
	at org.springframework.data.redis.connection.lettuce.LettuceConnection.lambda$doInvoke$4(LettuceConnection.java:919) ~[spring-data-redis-2.5.5.jar:2.5.5]
	at org.springframework.data.redis.connection.lettuce.LettuceInvoker$Synchronizer.invoke(LettuceInvoker.java:665) ~[spring-data-redis-2.5.5.jar:2.5.5]
	at org.springframework.data.redis.connection.lettuce.LettuceInvoker.just(LettuceInvoker.java:94) ~[spring-data-redis-2.5.5.jar:2.5.5]
	at org.springframework.data.redis.connection.lettuce.LettuceStringCommands.get(LettuceStringCommands.java:55) ~[spring-data-redis-2.5.5.jar:2.5.5]
	at org.springframework.data.redis.connection.DefaultedRedisConnection.get(DefaultedRedisConnection.java:267) ~[spring-data-redis-2.5.5.jar:2.5.5]
	at org.springframework.data.redis.core.DefaultValueOperations$1.inRedis(DefaultValueOperations.java:57) ~[spring-data-redis-2.5.5.jar:2.5.5]
	at org.springframework.data.redis.core.AbstractOperations$ValueDeserializingRedisCallback.doInRedis(AbstractOperations.java:60) ~[spring-data-redis-2.5.5.jar:2.5.5]
	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:222) ~[spring-data-redis-2.5.5.jar:2.5.5]
	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:189) ~[spring-data-redis-2.5.5.jar:2.5.5]
	at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:96) ~[spring-data-redis-2.5.5.jar:2.5.5]
	at org.springframework.data.redis.core.DefaultValueOperations.get(DefaultValueOperations.java:53) ~[spring-data-redis-2.5.5.jar:2.5.5]
	at com.jeeplus.common.redis.RedisUtils.get(RedisUtils.java:314) ~[classes/:na]
	......
	......
Caused by: io.lettuce.core.RedisException: java.io.IOException: 远程主机强迫关闭了一个现有的连接。
	at io.lettuce.core.internal.Exceptions.bubble(Exceptions.java:83) ~[lettuce-core-6.1.5.RELEASE.jar:6.1.5.RELEASE]
	at io.lettuce.core.internal.Futures.awaitOrCancel(Futures.java:250) ~[lettuce-core-6.1.5.RELEASE.jar:6.1.5.RELEASE]
	at io.lettuce.core.LettuceFutures.awaitOrCancel(LettuceFutures.java:74) ~[lettuce-core-6.1.5.RELEASE.jar:6.1.5.RELEASE]
	at org.springframework.data.redis.connection.lettuce.LettuceConnection.await(LettuceConnection.java:1060) ~[spring-data-redis-2.5.5.jar:2.5.5]
	... 54 common frames omitted
Caused by: java.io.IOException: 远程主机强迫关闭了一个现有的连接。
	at sun.nio.ch.SocketDispatcher.read0(Native Method) ~[na:1.8.0_171]
	at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:43) ~[na:1.8.0_171]
	at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223) ~[na:1.8.0_171]
	at sun.nio.ch.IOUtil.read(IOUtil.java:192) ~[na:1.8.0_171]
	at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:380) ~[na:1.8.0_171]
	at io.netty.buffer.PooledByteBuf.setBytes(PooledByteBuf.java:253) ~[netty-buffer-4.1.68.Final.jar:4.1.68.Final]
	at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1132) ~[netty-buffer-4.1.68.Final.jar:4.1.68.Final]
	at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:350) ~[netty-transport-4.1.68.Final.jar:4.1.68.Final]
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:151) ~[netty-transport-4.1.68.Final.jar:4.1.68.Final]
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719) ~[netty-transport-4.1.68.Final.jar:4.1.68.Final]
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655) ~[netty-transport-4.1.68.Final.jar:4.1.68.Final]
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581) ~[netty-transport-4.1.68.Final.jar:4.1.68.Final]
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493) ~[netty-transport-4.1.68.Final.jar:4.1.68.Final]
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986) ~[netty-common-4.1.68.Final.jar:4.1.68.Final]
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.68.Final.jar:4.1.68.Final]
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.68.Final.jar:4.1.68.Final]
	... 1 common frames omitted

2024-01-16 22:43:24.510  INFO 15328 --- [xecutorLoop-1-3] i.l.core.protocol.ConnectionWatchdog     : Reconnecting, last destination was /192.168.1.100:6379
2024-01-16 22:43:24.576  INFO 15328 --- [ioEventLoop-4-2] i.l.core.protocol.ReconnectionHandler    : Reconnected to 192.168.1.100:6379

出现原因 #

网络波动导致服务器主动断开连接,而 lettuce 客户端没有发现连接异常,导致下一次从连接池获取连接时报错。

详见博客: https://www.cnblogs.com/songjiyang/p/16809694.html 详见博客: https://www.cnblogs.com/wingcode/p/14527107.html

解决方案 A #

应用添加配置(仅对集群有效,要求 springboot 2.3 及以上):

yml
spring:
  redis:
    cluster:
      refresh:
        # 启用自适应拓扑刷新
        adaptive: true
        # 集群拓扑刷新间隔
        period: 20s

解决方案 B #

应用启用验证连接这项配置(每次使用连接时都会验证该连接是否可用,会影响性能):

java
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory factory) {
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    // 启用验证连接
    factory.setValidateConnection(true);
    redisTemplate.setConnectionFactory(factory);
    ...
    return redisTemplate;
}

解决方案 C #

使用 jedis 而不是默认的 lettuce(需要注意把 lettuce.pool 连接池配置改成 jedis.pool):

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

解决方案 D(待验证) #

给 lettuce 依赖的底层 netty 添加心跳机制(好像只是超时断连不是心跳???):

java
@Bean
public ClientResources clientResources() {
    NettyCustomizer nettyCustomizer = new NettyCustomizer() {
        @Override
        public void afterChannelInitialized(Channel channel) {
            // 此处必须小于超时时间
            int readerIdleTimeSeconds = 40;
            // 0表示禁用
            int writerIdleTimeSeconds = 0;
            int allIdleTimeSeconds = 0;
            channel.pipeline().addLast(
                new IdleStateHandler(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds));
            channel.pipeline().addLast(
                new ChannelDuplexHandler() {
                    @Override
                    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
                        if (evt instanceof IdleStateEvent) {
                            ctx.disconnect();
                        }
                    }
                }
            );
        }
    };

    return ClientResources.builder().nettyCustomizer(nettyCustomizer).build();
}

解决方案 E #

定时发送ping命令:

java
@Scheduled(fixedDelay = 60000)
public void redisPingTask() {
    redisTemplate.execute(new RedisCallback<Object>() {
        @Override
        public Object doInRedis(@NotNull RedisConnection connection) throws DataAccessException {
            return connection.ping();
        }
    });
}

或者定时验证连接:

java
@Scheduled(fixedDelay = 60000)
public void redisValidateConnectionTask() {
    lettuceConnectionFactory.validateConnection();
}
2025年1月17日