Redis异常 Can’t assign requested address
记一次很久之前的Redis连接异常。当时使用的是Redis Cluster集群,微服务的商品服务需要保存商品到Redis中,短时间内会连续执行HSET和HGET操作,程序启动跑了几分钟,就抛了一些错误异常,让我措手不及。异常信息如下:
Caused by: io.lettuce.core.RedisConnectionException: Unable to connect to 127.0.0.1<7994
at io.lettuce.core.RedisConnectionException.create RedisConnectionException.jav a:56
at io.lettuce.core.cluster.PooledClusterConnectionProvider.lambda$getConnectio nAsync$6 PooledClusterConnectionProvider.java:322
at java.util.concurrent.CompletableFuture.uniHandle CompletableFuture.java:822
at java.util.concurrent.CompletableFuture$UniHandle.tryFire CompletableFuture.ja va:797
at java.util.concurrent.CompletableFuture.postComplete CompletableFuture.java: 474
at java.util.concurrent.CompletableFuture.completeExceptionally CompletableFutu re.java:1977
at io.lettuce.core.AbstractRedisClient.lambda$initializeChannelAsync$1 AbstractR edisClient.java:275
at io.netty.util.concurrent.DefaultPromise.notifyListener0 DefaultPromise.java:511
at io.netty.util.concurrent.DefaultPromise.notifyListenersNow DefaultPromise.java: 485
at io.netty.util.concurrent.DefaultPromise.notifyListeners DefaultPromise.java: 424
at io.netty.util.concurrent.DefaultPromise.tryFailure DefaultPromise.java: 121
at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.connect Abstract NioChannel.java:290
at io.netty.channel.DefaultChannelPipeline$HeadContext.connect DefaultChannel Pipeline.java:1366
at io.netty.channel.AbstractChannelHandlerContext.invokeConnect AbstractChan
nelHandlerContext.java:545 at
io.netty.channel.AbstractChannelHandlerContext.connect AbstractChannelHan dlerContext.java:530
at io.netty.channel.ChannelDuplexHandler.connect ChannelDuplexHandler.java: 50
at io.netty.channel.AbstractChannelHandlerContext.invokeConnect AbstractChan nelHandlerContext.java:545
at io.netty.channel.AbstractChannelHandlerContext.connect AbstractChannelHan dlerContext.java:530
at io.netty.channel.ChannelOutboundHandlerAdapter.connect ChannelOutboundH andlerAdapter.java:47
at io.netty.channel.AbstractChannelHandlerContext.invokeConnect AbstractChan nelHandlerContext.java:545
at io.netty.channel.AbstractChannelHandlerContext.connect AbstractChannelHan dlerContext.java:530
at io.netty.channel.ChannelDuplexHandler.connect ChannelDuplexHandler.java: 50
at io.netty.channel.AbstractChannelHandlerContext.invokeConnect AbstractChan nelHandlerContext.java:545
at io.netty.channel.AbstractChannelHandlerContext.connect AbstractChannelHan dlerContext.java:530
at io.netty.channel.AbstractChannelHandlerContext.connect AbstractChannelHan dlerContext.java:512
at io.netty.channel.DefaultChannelPipeline.connect DefaultChannelPipeline.java: 1024
at io.netty.channel.AbstractChannel.connect AbstractChannel.java:259 at io.netty.bootstrap.Bootstrap$3.run Bootstrap.java:252
at
io.netty.util.concurrent.AbstractEventExecutor.safeExecute AbstractEventExecu tor.java:163
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks SingleThreadEv entExecutor.java:404
at io.netty.channel.nio.NioEventLoop.run NioEventLoop.java:463
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run SingleThreadEventEx ecutor.java:886
at io.netty.util.concurrent.FastThreadLocalRunnable.run FastThreadLocalRunnable .java:30
at java.lang.Thread.run Thread.java:748
Caused by: java.util.concurrent.CompletionException: io.netty.channel.AbstractChannel$AnnotatedSocketException: Can't assign requested address: /127.0.0.1<7994
at java.util.concurrent.CompletableFuture.encodeThrowable CompletableFuture.ja va:292
at java.util.concurrent.CompletableFuture.completeThrowable CompletableFuture. java:308
at java.util.concurrent.CompletableFuture.uniApply CompletableFuture.java:593
at java.util.concurrent.CompletableFuture$UniApply.tryFire CompletableFuture.jav a:577
... 30 common frames omitted
Caused by: io.netty.channel.AbstractChannel$AnnotatedSocketException: Can't assign requested address: /127.0.0.1<7994
at sun.nio.ch.Net.connect0 Native Method
at sun.nio.ch.Net.connect Net.java:454
at sun.nio.ch.Net.connect Net.java:446
at sun.nio.ch.SocketChannelImpl.connect SocketChannelImpl.java:648 at io.netty.util.internal.SocketUtils$3.run SocketUtils.java:83
at io.netty.util.internal.SocketUtils$3.run SocketUtils.java:80 at java.security.AccessController.doPrivileged Native Method at io.netty.util.internal.SocketUtils.connect SocketUtils.java:80 at
io.netty.channel.socket.nio.NioSocketChannel.doConnect NioSocketChannel.ja va:310
at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.connect Abstract NioChannel.java:254
... 22 common frames omitted
Caused by: java.net.BindException: Can't assign requested address
... 32 common frames omitted
原因是 按单条商品记录保存到Redis,hset(key, field, value),而且是两个线程同时操作不同商品插入Redis与从Redis获取商品,这个操作是单条命令连续执行,而不是批量保存和批量获取,另外在构建Lettuce客户端是没有使用线程连接池,最终导致Lettuce Redis客户端频繁连接Redis服务器,由于每次连接短时间内结束,导致很多TCP TIME_WAIT,这样旧连接还没结束,连接数有限,新连接没办法绑定端口,即Cannot assign requested address
netstat -nat | grep 127.0.0.1:7994 会看到连接127.0.0.1:7994的状态,你会发现很多TIME_WAIT
为什么单个客户端多次操作,会出现这么多连接了呢?
这是lettuce redis没有完善的地方:传送门
要到Spring Data Redis 2.1 发布才行。
其中的spring boot redis maven依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis lettuce 客户端需要的依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>
解决方案:
把单条执行改批量执行,如HSET改为HMSET,HGET改HMGET
需要加redis 线程连接池。幸好spring boot redis 已经帮我们构建好了一个LettuceConnectionFactory连接工厂Bean,该工厂类Bean已经实现了连接池。
直接依赖注入就行。
附上实现代码:
package xxxx.xxxx.xxxx.xxxx; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.util.List; import java.util.Objects; import lombok.Data; @Configuration @Data public class RedisClusterConfig { private LettuceConnectionFactory lettuceConnectionFactory; @Autowired public RedisClusterConfig(LettuceConnectionFactory lettuceConnectionFactory) { // 依赖注入LettuceConnectionFactory工厂类 // LettuceConnectionConfiguration自动注入了含lettuce Pool连接池和集群Cluster的LettuceConnectionFactory连接工厂类 this.lettuceConnectionFactory = lettuceConnectionFactory; } @Bean RedisTemplate<String, Object> lettuceRedisTemplate() { // 设置序列化 Jackson2JsonRedisSerializer<String> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<String>(String.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.enableDefaultTyping(DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); // 配置redisTemplate RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>(); redisTemplate.setConnectionFactory(lettuceConnectionFactory); RedisSerializer<?> stringSerializer = new StringRedisSerializer(); // key 序列化 redisTemplate.setKeySerializer(stringSerializer); // value 序列化 redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // Hash Key序列化 redisTemplate.setHashKeySerializer(stringSerializer); // Hash Value 序列化 redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); // 初始化赋值 redisTemplate.afterPropertiesSet(); return redisTemplate; } }