项目背景
我们的项目使用redis的场景主要是有两种,一是使用redis缓存各种业务信息,二是使用redis做分布式锁。主要是引用了两个框架jedis和redisson。
Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;
Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为复杂,不仅支持字符串操作,且还支持排序、事务、管道、分区等Redis特性。
问题现象
生产中发现有业务异常,排查错误日志之后发现有以下报错:
# *为ip,安全需要默认过滤
RedisConnectionClosedException:Command succesfully sent, but channel [id: 0x76e20cc7,*] has been。
影响范围
使用到redis的场景都可能会出问题。
解决方案
修改pingConnectionInterval配置为60000,默认是0。该配置主要是用于客户端和redis做心跳探活。
问题分析
Redis是通过Redis协议(RESP)进行通信的,而Redis协议位于TCP协议之上,客户端与服务端保持双工连接。简单来说就是客户端和服务端可以双向通信,客户端收发消息,服务端收发消息。
而我们都知道TCP是通过三次握手建立连接,4次挥手释放连接的。
第一次握手:
客户端将TCP报文标志位SYN置为1,随机产生一个序号值seq=J,保存在TCP首部的序列号(Sequence Number)字段里,指明客户端打算连接的服务器的端口,并将该数据包发送给服务器端,发送完毕后,客户端进入SYN_SENT状态,等待服务器端确认。
第二次握手:
服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将TCP报文标志位SYN和ACK都置为1,ack=J+1,随机产生一个序号值seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。
第三次握手:
客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。
挥手请求可以是Client端,也可以是Server端发起的,我们假设是Client端发起:
-
第一次挥手: Client端发起挥手请求,向Server端发送标志位是FIN报文段,设置序列号seq,此时,Client端进入FIN_WAIT_1状态,这表示Client端没有数据要发送给Server端了。
-
第二次分手:Server端收到了Client端发送的FIN报文段,向Client端返回一个标志位是ACK的报文段,ack设为seq加1,Client端进入FIN_WAIT_2状态,Server端告诉Client端,我确认并同意你的关闭请求。
-
第三次分手: Server端向Client端发送标志位是FIN的报文段,请求关闭连接,同时Client端进入LAST_ACK状态。
-
第四次分手 : Client端收到Server端发送的FIN报文段,向Server端发送标志位是ACK的报文段,然后Client端进入TIME_WAIT状态。Server端收到Client端的ACK报文段以后,就关闭连接。此时,Client端等待2MSL的时间后依然没有收到回复,则证明Server端已正常关闭,那好,Client端也可以关闭连接了。
在正常操作中,我们观察到连接的建立和释放过程通常能够顺畅进行。然而,若服务器意外断网或服务非预期中断,Redis客户端便无法实时检测到连接已断开,因此不能立即执行释放操作。只有在后续尝试使用该连接时,客户端才能意识到连接已经失效。
问题复现
-
- 正常启动redis服务和应用服务;
-
- 关闭redis服务;
-
- 使用netstat观察服务器上所有的连接
netstat |grep 6379
- 如下图,我们能观察到连接最后有ESTABLISHED,表示的是端口的状态。字典值有以下值:LISTENING监听状态;ESTABLISHED连接状态;CLOSE_WAIT主动关闭连接或者网络异常导致连接中断状态;TIME_WAIT主动调用close()断开连接,收到对方确认后状态变为TIME_WAIT。
-
- 等到所有连接都消失以后,重启redis服务;
-
- 调用客户端服务接口,当需要使用redis时就会报错RedisConnectionClosedException了。
后续改进措施
为了有效地运用各种组件,深入理解组件的具体细节是必不可少的,这包括它们所依赖的协议等技术要素。要想预防TCP连接异常这类问题,对底层技术的敏感性尤为重要。实际上,不仅是Redis,项目中所采用的众多组件也通常内置有类似的机制。然而,考虑到性能因素,许多框架默认不启用连接探测功能,例如常用的数据库连接池Druid等也是如此。