Redis中的RPC协议结构
Redis中的RPC协议结构
Redis的客户端和服务器之间进行RPC通信时,使用的是其开发者专门设计的文本通讯协议RESP。它的
开发者认为数据库系统的性能瓶颈一般不在于网络流量,而是数据库自身内部的逻辑处理上。所以Redis使
用了浪费流量的文本协议。但是,依然可以取得很高的访问性能
下面主要介绍RESP协议的相关内容
一、RESP协议简介
RESP(Redis Serialization Protocol),就是Redis的序列化协议,这是一种对人友好的文本协议。它将
传输的结构数据分为5种最下单元的类型,单元结束时统一加上回车换行符\r\n,具体如下
单行字符串以+符号开头
+hello world\r\n
多行字符串以$符号开头,后跟字符串的长度
$11\r\nhello world\r\n #长度加上字符串
整数值以:符号开头,后跟整数的字符串形式
:1024\r\n
错误的消息以 - 开头
-WRONGTYPE Operation against a key holding the wrong kind of value\r\n
数组以*开头,后面跟数组的长度
*3\r\n:1\r\n:2\r\n:3\r\n #先是数组长度,依次再是每个内容,表示数组[1,2,3]
NULL用多行字符串表示,不过长度要写成-1
$-1\r\n
空串用多行字符串表示,长度填0
$0\r\n\r\n #两个\r\n之间隔的是空串
客户端向服务器发送指令使用多行字符串数组,比如一个简单的set指令 set author codehole会被序列化为
下面的字符串
*3\r\n$3\r\nset\r\n$6\r\nauthor\r\n$8\r\ncodehole\r\n
服务端向客户端回复的响应要支持多种数据结构,基本上都是上述5种基本类型的组合
#单行字符串响应 >set author codehole OK #这里的OK就是单行响应,没有使用引号括起来 #错误响应 -ERR ........ #整数响应 >incr books (integer) 1 #多行字符串的响应 >get author "codehole" #使用双引号括起来的字符串就是多行字符串响应
二、Redis协议的缺陷
1.连接重连
RPC协议是建立在TCP协议基础上进行消息传递的,而TCP连接并不总是稳定的,非常容易受到网络的影
响而断开。与此同时大部分的服务器也会限制空闲连接的生存期,如果一个TCP连接闲置过久,也会被服务器
主动关闭。当RPC连接断开后,客户端一般都需要实现连接重连,否则客户端无法继续与RPC交互
2.请求重试
当RPC客户端向服务器发送请求之后,连接突然断开,这个断开可能发生在请求阶段(服务端没有收
到消息);也可能发生在响应阶段(服务器已经处理了消息),这时客户端没有收到回复。所以客户端不
知道服务器是否已经处理了消息还是根本就没有收到。客户端会尝试重连,连接成功后重试之前的请求,
但是客户端确定服务端是否会重复执行请求
为了解决请求可能被重试的情况,客户端在构造请求时需要为每一个请求赋予一个唯一ID
class Request{
UUID id; //每个请求都有一个唯一的ID
}
服务端在接收到这个请求后会记录这个ID,和对应的响应关联起来,再次遇到该请求时,会将前面的响应
记录下来直接进行回复。但是服务器的内存是有限的,不能记录所有的请求ID和响应,一般只会保留最近一段
时间的请求ID和响应
class Server{
Set<RequestID> pendingRequests; //正在处理的请求
Cache<RequestId,Response> cachedResponses; //已经处理完毕的请求
}
3.Redis客户端的缺陷
我们上述提到的唯一请求ID的问题,Redis并没有解决,只是在简单地遇到异常时重试一下
客户端源代码如下:
def execute_command(self,*args,**options):
#执行命令返回解析后的响应
poll = self.connection_pool
command_name = args[0]
connection = pool.get_connection(command_name,**options)
try:
connection.send_command(*args) #发送请求
return self.parse_response(connection,command_name,**options) #接收并解析请求
except(ConnectionError,TimeoutError) as e:
connection.disconnect() #先关闭异常连接
if not connection.retry_on_timeout and isinstance(e,TimeoutError):
raise connection.send_command(*args) #重试
return self.parse_response(connection,command_name,**options)
finally:
pool.release(connection) #连接被连接池回收
ConnectionError是在建立连接时就出了错误,或者从连接池中无法获取连接,这是需要重试的
TimeoutError错误的出现可能是在写消息时遇到了超时,或者可能在读消息时出现超时
写超时是指内核为当前套接字开辟的写缓存空间已经满了,主要有三种原因导致:
写方的消息因为网络原因无法到达读的一方
读方老师不读消息,没有办法给写方ACK确认
写方因为网络原因无法收到对方的ACK确认
在TCP的超时重传策略中必须要求收到读方的ack之后才可以将数据从写缓存中删除,否则会继续留在
写缓冲区中以便后续可能的TCP重传
读超时是指消息已经写进本地写缓冲区中了,但是另一方调用recv方法时无法收到消息
总结来说就是Redis客户端的代码在进行重试时不能分清楚请求错误或者超时的原因