Redis客户端和Redis服务器通过一个叫做RESP(REdis Serialization Protocol,Redis序列化协议)的协议进行通讯。虽然这个协议是为Redis设计的,但是它也能被用在其它的客户端-服务器软件项目。

RESP是以下几个方面妥协的结果:

  • 易于实现
  • 快速解析
  • 可读性好

RESP可以序列化不同的数据类型,比如整型,字符串,数组。另外还有特定的类型表示错误。请求由客户端以字符串数组(要执行的命令的参数)的形式发往Redis服务器。Redis回复一个和命令对应的数据类型。

RESP二进制安全的,并且不需要处理进程间传输的批量数据,因为它使用prefixed-length来传输批量数据。

Note:这里讨论的协议仅仅用于客户端-服务器通信。Redis集群使用一个不同的二进制协议来交换不同节点之间的消息。

网络层

一个客户端连接到一个Redis服务器,就是创建一个TCP连接。尽管RESP是和TCP独立的,到那时在Redis的上下文下面,我们仅仅和TCP(或者等价的像Unix套接字这样的面向流的连接)一起使用。

请求相应模型

Redis接收由不同参数组成的命令。一旦一个命令接收到,它被处理并且一个reply会被发送回客户端。这是可能的最简单的模型,然而有两个例外:

  • Redis支持管道(会在这个本章的后面提到)。也就是说,一个客户端可以一次性发送多个命令,然后等待replies。
  • 当一个Redis客户端订阅一个Pub/Sub channel的时候,这个协议改变语义,变成push协议。也就是说,客户端不再需要发送命令,因为服务器只要接受到信息就会自动给客户端发送消息(对于客户端订阅的channels)。

除了上述两个例外,Redis协议就是一个请求相应协议。

RESP协议描述

RESP协议在Redis1.2的时候开始引入,在Redis2.0的时候成为和Redis服务器通信的标准。这是一个你需要在你的Redis客户端中实现的协议。

RESP实际上是一个支持一下数据类型的序列化协议:Simple Strings,Errors,Integers,Bulk strings 和Arrays。

RESP在Redis下面被应用为请求相应协议的方式如下:

  • 客户端给Redis服务器发送的commands:一个RESP Bulk string数组。
  • 根据command的实现,服务器会回复一个RESP数据类型。

在RESP,数据的类型由第一个字节决定:

  • Simple string:“+”。
  • Errors:“-”。
  • Integer:“:”。 Bulk strings:“$”。 Arrays:“*”。 另外,RESP可以通过一个Bulk string或者Array的变体表示Null。在RESP,协议的不同部分都会用“\n\r”(CRLF)来表示结尾。

RESP simple strings

Simple strings 通过以下方式编码:一个加号,string(不能包含CR和LF),由CRLF终止。

Simple strings被用来以最小的overhead传输非二进制安全的字符串。比如,很多Redis命令会回复“OK”当执行成功的时候。这个时候就是RESP Simple strings:

“+OK\r\n”

如果要发送二进制安全的字符串就要使用RESP bulk strings。

当Redis用一个simple string回复的时候,一个客户端应该返回给调用者一个由“+”开始的字符串,但是省略CRLF。

RESP Errors

RESP有一个专门针对Errors的数据类型。事实上errors真的很像RESP simple string,但是第一个字符是一个“-”。errors和simple string真正的差异是对于客户端来说的,Errors被当作一种异常,其中的string被当作错误消息。基本的形式如下:

"-Error message\r\n"

Error只有当有错误出现的时候才会由Redis发送,比如你在一个错误的数据类型上进行一个操作,或者command不存在等等。客户端应当在收到Error回复的时候抛出异常。以下是Errors的例子:

-ERR unknown command 'foobar'
-WRONGTYPE Operation against a key holding the wrong kind of value

“-”后面第一个word表示错误的类型,这只是Redis的一个convention不是RESP error format。

比如,ERR是一个general的错误,WRONGTYPE是一个更加specific的错误(意味着客户端正在尝试对一个错误的数据类型进行某种操作)。这被叫做Error Prefix,可以帮助客户端理解错误。

一个客户端实现可能针对不同的错误实现不同的异常,也有可能仅仅是将Redis返回的信息作为一个字符串返回给调用者。however,这样的特性可能并不是很重要,因为没什么用,有些简单的客户端可能仅仅返回false给客户端。

RESP integers

这个类型就是一个CRLF终止的字符串,以“:”为前缀。比如“:0\r\n”,“:1000\r\n”就是整型回复。

很多Redis命令返回RESP整型,比如INCR,LLEN和LASTSAVE。

返回的整型数没有什么特殊的意义,对于INCR来说就是一个增量后的数据,对于LASTSAVE就是一个UNIX时间等等。但是返回的整型数据保证是一个64位的有符号整数。整数也被大量用于返回true和false。比如EXISTS或者SISMEMEBER会返回1表示true,返回0表示false。

其它像SADD,SREM,SETNX这些命令当成功的时候会返回1,失败返回0。

以下命令会产生一个整数回复:SETNX,DEL,EXISTS,INCR,INCRBY,DECR,DECRBY,DBSIZE,LASTSAVE,RENAMENX,MOVE,LLEN,SADD,SREM,SISMEMBER,SCARD。

RESP Bulk strings

bulk string是用来表示单个二进制安全的字符串(最大512MB)。Bulk string通过以下方式编码:

  • 一个“$”开始,然后跟着字符串长度(也就是prefixed length),然后用CRLF终止。
  • 实际字符串数据
  • CRLF

所以字符串“foobar”编码如下:

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

空字符串:

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

RESP bulk string 还可以通过是用一种特殊的形式表示一个不存在的值,也就是Null。这种特殊的形式有长度-1,没有数据,所以Null可以表示为:

"$-1\r\n"

这个被叫做Null Bulk string。

当server回复一个Null Bulk string数据类型的时候,客户端API应该返回一个nil对象,而不是一个空字符串。比如一个Ruby客户端应该返回'nil',而一个C库应该返回NULL(或者在回复对象中设置一个特殊的标志)等等。

RESP数组(译者注:类似C中的结构体)

客户端通过RESP Arrays给Redis服务器发送命令。相似的,一些Redis命令会使用Arrays的形式从服务器得到返回值。比如,LARANGE命令。

RESP数组通过以下形式发送:

  • 一个“*”作为第一个字节,紧跟一个十进制数用于表示数组中元素的数量,以CRLF结尾。
  • 另外的RESP类型数据来表示数组中的元素。

所以一个空的数组可以表示为:

"*0\r\n"

而一个由2个bulk string,“foo”和“bar”组成的数组可以表示为:

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

正如你所看到的,除了前缀之外后面就是由其它数据类型一个一个连接而成的。比如一个由3个整数组成的数组编码为:

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

数组不需要每个数组元素都是同一类型的,可以包换混合类型的元素。比如,4个整数和一个bulk string可以编码如下:

*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个元素会发送。然后发送这5个元素。

Null数组的概念也存在,并且是另一种方式来表示Null值。(Usually会用Null bulk string,但是由于历史原因我们保留两种形式的Null)。比如当BLPOP timeout的时候,它返回一个Null数组,它带有-1计数:

"*-1\r\n"

一个客户端得到一个Null数组的回复的时候,需要返回一个null对象,而不是一个空数组。当用于区分一个空字符串和其它事件(比如BLPOP的timeout)的时候这就变得很有必要。

RESP也允许数组的数组。比如一个由两个数组组成的数组:

*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数据编码了一个由2个数组组成的数组,一个数组由整数1,2,3组成,另一个数组由一个simple string和一个errror组成。

字符串中的Null元素

数组中的某个元素可能是Null。这可以用来表示有一个元素缺失,但不是空元素。这个可以出现,比如SORT命令结合GET命令一起使用的时候。包含一个Null元素的数组的例子:

*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客户端应该是很容易的。我们可以进一步描述一下客户端和服务器之间的交互:

  • 客户端给Redis服务器发送一个仅仅由bulk string组成的RESP数组。
  • 一个服务器给客户端回复任意有效的RESP数据类型。

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

客户端发送一个命令LLEN mylist,来获取存在key mylist的list的长度。服务器回复一个整数回复(C:客户端,S:服务器):

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这样一个整体。

多个命令和管道

一个客户端可以使用相同的连接来发送多个命令。Redis支持管道,从而支持客户端通过一个写操作来发送多个命令,而不需要读取服务器对于上一个命令的回复再发送下一个命令。所有的回复都可以在最后读取。更堵的信息可以查阅page about Pipelining。

Inline命令

有时候,你手上只有Telnet,但是你想向Redis server发送命令。尽管Redis协议实现起来很简单,但是在交互式会话下面还是不理想。同事,redis-cli又不可用。出于这个原因,Redis也通过一个特殊的方式接收命令。这种方式是为人设计的,叫做inline 命令形式。

以下就是一个使用inline命令来进行server/client对话的例子。

C: PING
S: +PONG

以下是另一个例子:使用inline命令返回一个整数

C: EXISTS someky
S: :0

基本上,在telnet会话下面,你只是写了以空格分隔的参数。因为没有命令以*开始,所以Redis使用unified request protocol,因此redis有能力检测这个情况,并且解析你的命令。

Redis协议的高性能解释器

Redis协议是一个可读性好,而且可以很容易实现为像二进制协议那样快速的高性能协议。

RESP使用前缀长度来传输bulk数据,所以不需要像JSON那样扫描特殊字符,或者quote来确定payload。

Bulk和多Bulk长度可以被程序使用,比如,在C语言下:

#include <stdio.h>

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之后退出。然后bulk数据就可以只需要读一次就可以了,不需要再次检查payload的长度。最后的CR和LF将被直接discard。

在用用二进制协议一样的性能的前提下,Redis协议可以在大部分高级语言下面很方便的实现,因此减少客户端软件中的bug。