Redis客户端使用称为RESP(REdis序列化协议)的协议与Redis服务器进行通信。虽然该协议是专为Redis设计的,但它可以用于其他C/S架构的软件项目。

RESP结合了以下优点:

易于实现

解析速度快

可读性强

RESP能够序列化诸如integers、 strings、arrays的数据类型,也有一种特殊的类型用于表示错误。请求以字符串数组的形式从客户端发送到Redis服务器,这些字符串表示要执行的命令的参数,Redis服务器回复一个被命令指定的数据类型。

RESP是二进制安全的,并且不需要处理从一个进程传输到另一个进程的块数据,因为它使用了前置长度(prefixed-length)来传输大量数据(就像HTTP中的Content-Type)。

网络层

客户端通过TCP连接到服务器的6379端口。尽管RESP在技术上不是局限于TCP的,但是在实际场景中Redis协议只使用TCP连接(或者像Unix套接字一样的面向流的连接)。

请求-响应 模型

Redis接收由不同参数组成的命令,当一条命令被接收,会被马上处理并发送响应包给客户端。

这可能是最简单的模型,但是有两个例外:

Redis支持管道(在文档后面介绍),所以客户端能够一次发送多条命令,然后等待服务器响应。

当一个Redis客户端订阅了Redis的Pub/Sub/频道(Publish, Subscribe),协议会改变语义,变成一个推送协议。这意味着客户端不再需要发送命令,因为服务器会自动地向客户端推送接收到的新消息。

排除以上两个例外,Redis协议是一个简单的 请求-响应式 协议。

RESP协议描述

RESP协议在Redis 1.2版本中被引入,但是在Redis 2.0中成为了与服务器进行通信的标准。所以你应该在你的Redis客户端中实现此协议。

RESP实际上是支持这些数据类型的序列化协议:Simple Strings、Errors、Integers、Bulk Strings 和 Arrays。

RESP以以下几种形式在Redis中作为请求-响应式协议使用:

客户端以大文本数组的形式向Redis服务器发送命令。

服务器根据命令响应一种RESP类型。

在RESP中,某些数据的类型取决于第一个字节:

对于Simple Strings,答复的第一个字节为”+”。

对于Errors,答复的第一个字节为”-“。

对于Integers,答复的第一个字节为”:”。

对于Bulk Strings,答复的第一个字节为”$”。

对于Arrays,答复的第一个字节为”*”。

另外,RESP可以使用特殊形式的Bulk Strings或Arrays来表示Null值。RESP协议中的不同部分使用\r\n分隔(CRLF)。

RESP Simple Strings

Simple Strings通过以下几种方式被编码:一个”+”,后面跟着字符串(不能包含\r\n,也不能包含换行),以\r\n(CRLF)结尾。

Simple Strings是一种被用来传输非二进制安全文本的最小开销方法。比如,很多Redis服务器在命令执行成功时返回一个”OK”,那是被5个字节编码的Simple Strings:

"+OK\r\n"。

RESP Bulk Strings是发送二进制安全字符串的更好的替代品。

当Redis返回一个Simple Strings时,客户端的库应该为调用者返回一个字符串,该字符串由”+”之后直到末尾的字符组成,不包括最后的CRLF字节。

RESP Errors

RESP为错误指定了一个数据类型。实际上Errors很像Simple Strings,但是第一个字节是”-“而不是”+”。RESP中,Errors和Simple Strings真正的不同是,Errors会被客户端当作异常处理。由Error类型组成的字符串是错误消息本身。

基本格式如下:

"-Error message\r\n"

仅当错误发生时,服务器才会发送Errors响应,例如客户端尝试对错误的数据类型执行操作,或者命令不存在等等。当一个Error类型被客户端接收时,客户端库应该抛出一个异常。

下面是错误响应的例子:

-ERR unknown command 'foobar'

-WRONGTYPE Operation against a key holding the wrong kind of value

从”-“之后直到第一个空格或者换行,代表返回的错误类型。这只是被Redis所使用的一种约定,不是RESP Errors格式的一部分。

例如,”ERR”代表常规错误。”WRONGTYPE”是一种更为详细的错误,说明客户端正在尝试对错误的数据类型执行操作。这被称为Errors Prefix(错误消息前缀),是一种是客户端在无需给定确切信息的情况下理解服务端返回的错误类型的方式,随着时间的推移,这一点可能会被改变。

一个客户端的实现可能会为不同的Errors抛出不同的错误,或者以更为常规的方式捕获错误,直接给调用者返回字符串类型的错误消息。

然而,这样的功能不应该被认为是至关重要的,因为它很少有用,而且有限的客户端实现可能仅仅返回一个通用的错误条件,比如false。

RESP Integers

这个类型只是用以CRLF结尾的字符串代表整数,前缀字节是”:”,例如”:0\r\n”,或者”:1000\r\n”,都是整数响应。

很多Redis命令返回一个RESP Integers,比如INCR,LLEN和LASTSAVE。返回的整数没有什么特殊意义,INCR仅仅是一个增量数字,LASTSAVE是一个Unix时间戳,等等。然而,返回的整数被确保在有符号64位整数范围内。

Integer响应也被广泛应用于返回布尔值,true或者false。例如,EXISTS或者SISMEMBER命令会返回1来代表true,0代表false。其他的命令,像SADD,SREM和SETNX,会在操作执行成功时返回1,否则返回0。

以下命令会返回Integers类型响应:SETNX,DEL,EXISTS,INCR,INCRBY,DECR,DECRBY,DBSIZE,LASTSAVE,RENAMENX,MOVE,LLEN,SADD,SREM,SISMEMBER,SCARD。

RESP Bulk Strings

Bulk Strings被用来表示一个二进制安全的字符串,长度在512MB以内。

Bulk Strings通过以下方式被编码:

开头的”$”字节,后面跟着被包含字符串的字节数(prefixed-length),以CRLF结尾。

实际的字符串数据。

末尾的CRLF。

所以字符串”foobar”编码后是这样:

"$6\r\nfoobar\r\n"

一个简单的空字符串是这样:

"$0\r\n\r\n"

RESP Bulk Strings也能够用特殊的格式来表示Null值,代表不存在的值。在这个特殊格式中,长度是-1,没有实际数据,所以Null被表示为如下形式:

"$-1\r\n"

这被叫做Null Bulk String。

当服务器返回一个Null Bulk String,客户端库的API不应该返回一个空字符串,应该返回一个nil对象。例如Ruby库应该返回一个nil,C语言库应该返回一个NULL(或者在响应中设置特殊标记),等等。

RESP Arrays

客户端通过RESP Arrays给服务端发送一系列命令。类似的,对于一些Redis命令,会使用RESP Arrays类型返回元素的集合。典型例子是LRANGE命令,返回一个元素列表。

RESP Arrays通过以下格式被发送:

开头的”*”字节,后面跟着数组中元数个数的十进制数,最后是CRLF。

数组中每个元素的RESP类型。

所以一个简单的Array看起来像这样:

"*0\r\n"

包含两个Bulk Strings的数组编码后是这样:

"*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"

如你所见,*CRLF在数组最前,其他的数据类型和数据一个接一个跟在后面。例如,包含三个整数的数组编码后是这样:

"*3\r\n:1\r\n:2\r\n:3\r\n"

Arrays能够包含不同的类型,元素没有必要都是同种类型。例如,一个包含四个整数和一个大块字符串的数组,编码后是这样:

*5\r\n

:1\r\n

:2\r\n

:3\r\n

:4\r\n

$6\r\n

foobar\r\n

服务器发送的第一行是*5\r\n,以便指定随后将有5个响应,然后构成响应的五行依次被传输。

Null Array的概念也存在,并且有可选的方式用于指定Null值(通常使用Bulk Null String,但是由于历史原因,有两种格式)。

例如,当BLPOP命令超时,会返回一个数目为-1的Null Array:

"*-1\r\n"

当Redis回复一个Null Arrays的时候,客户端库的API应该返回一个null对象而不是空数组。有必要将空列表和不同情况(比如BLPOP命令超时的情况)区分开。

RESP中允许出现包含Array的Array。例如,包含两个Array的Array编码后如下:

*2\r\n

*3\r\n

:1\r\n

:2\r\n

:3\r\n

*2\r\n

+Foo\r\n

-Bar\r\n

上面的RESP数据类型编码了由两个Array构成的Array,其中一个包含三个Integers 1,2,3,另一个包含一个Simple String和Error。

Arrays中的Null元素

Array中的单个元素可以为Null。这在Redis响应中用来表示这些元素是缺失的,不是空字符串。当与GET模式选项一起使用SORT命令时,如果缺少指定的键,就会发生这种情况。

这是一个包含Null元素Array的例子:

*3\r\n

$3\r\n

foo\r\n

$-1\r\n

$3\r\n

bar\r\n

第二个元素为Null,客户端库应该返回类似这样的内容:

["foo",nil,"bar"]

请注意,这并不是前几节中所说的例外,而是一个进一步指定协议的示例。

向Redis服务器发送命令

现在你已经熟悉了RESP序列化的格式,写一个Redis客户端库的实现将会非常轻松。我们来深入地了解一下客户端与服务器是怎样交互的:

客户端发送由Bulk Strings组成的RESP Array。

Redis服务器向任何发送有效RESP数据类型的客户端发出响应。

所以一个典型的交互大概是这样:

客户端发送LLEN mylist命令去获取键mylist中存储的列表的长度,服务器返回一个Integer类型的响应,就像下面这样:

C: *2\r\n

C: $4\r\n

C: LLEN\r\n

C: $6\r\n

C: mylist\r\n

S: :48293\r\n

通常情况下,为了简单,会使用换行来分隔协议中的每一部分,但是实际交互中,客户端发送了:

*2\r\n$4\r\nLLEN\r\n$6\r\nmylist\r\n

多行命令和管道

一个客户端可以使用相同的连接来发送多行命令。管道可以使客户端一次发送多行命令,而不需要一条一条去读取前一个命令的响应。所以的响应可以在最后一起读取。

内联命令

有些时候你手上只有telnet可以用,但是需要发送向Redis服务器发送命令。虽然Redis协议很容易实现,但在交互会话中使用可能并不理想,而且redis-cli可能并不总是在手边。出于这个原因,Redis也以特殊的友好方式接受命令,它被称作内联命令格式。

以下是一个客户端与服务器使用内联命令进行交互的例子:

C: PING

S: +PONG

以下是另一个返回Integer的内联命令例子:

C: EXISTS somekey

S: :0

基本上,您只需在telnet会话中编写以空格分隔的参数。即使请求的协议中没有以*开头的命令,Redis也能够检测出来并解析你的命令。

Redis协议高性能解析器

虽然Redis协议可读性很强并且容易去实现,但是它的性能可以和二进制协议相媲美。

RESP使用prefixed lengths(前置的长度)来传输块数据,所以从不需要去像JSON一样去检查载体中是否存在特殊字符,也不需要将他们用引号包裹起来。

Bulk和Multi Bulk的长度可以被这样一段代码处理,在对每个字符进行操作的同时扫描CR字符,就像这样:

#include

int main(void) {

unsigned char *p = "$123\r\n";

int len = 0;

p++;

while(*p != '\r') {

len = (len*10)+(*p - '0');

p++;

}

/* Now p points at '\r', and the len is in bulk_len. */

printf("%d\n", len);

return 0;

}

当第一个CR被识别出后,剩下的LF可以跳过而不做任何处理。然后块数据可以被一次读取,而不需要验证消息载体。最终,剩余的CR和LF可以被无条件丢弃。

二进制协议和Redis协议在性能上相比,Redis协议明显在很多高级语言中更容易实现,减少了客户端软件中bug的数量。