1. RESP
Redis
客户端与Redis
服务端基于一个称作RESP
的协议进行通信,RESP
全称为Redis Serialization Protocol
,也就是Redis
序列化协议。虽然RESP
为Redis
设计,但是它也可以应用在其他客户端-服务端(Client-Server
)的软件项目中。RESP
在设计的时候折中考虑了如下几点:
- 易于实现。
- 快速解析。
- 可读性高。
RESP
可以序列化不同的数据类型,如整型、字符串、数组还有一种特殊的Error
类型。需要执行的Redis
命令会封装为类似于字符串数组的请求然后通过Redis
客户端发送到Redis
服务端。Redis
服务端会基于特定的命令类型选择对应的一种数据类型进行回复(这一句是意译,原文是:Redis replies with a command-specific data type
)。
RESP
是二进制安全的(binary-safe
),并且在RESP
下不需要处理从一个进程传输到另一个进程的批量数据,因为它使用了前缀长度(prefixed-length
,后面会分析,就是在每个数据块的前缀已经定义好数据块的个数)来传输批量数据。
注意:此处概述的协议仅仅使用在客户端-服务端通信,Redis Cluster
使用不同的二进制协议在多个节点之间交换消息(也就是Redis
集群中的节点之间并不使用RESP
通信)。
2. 网络层
Redis
客户端通过创建一个在6379
端口的TCP
连接,连接到Redis
服务端。
虽然RESP
在底层通信协议技术上是非TCP
特定的,但在Redis
的上下文中,RESP
仅用于TCP
连接(或类似的面向流的连接,如Unix
套接字)。
客户端和服务器发送的命令或数据一律以 \r\n (CRLF)结尾。
3. 请求-响应模型
Redis
服务端接收由不同参数组成的命令,接收到命令并将其处理之后会把回复发送回Redis
客户端。这是最简单的模型,但是有两种例外的情况:
Redis
支持管道(Pipelining
,流水线,多数情况下习惯称为管道)操作。使用管道的情况下,Redis
客户端可以一次发送多个命令,然后等待一次性的回复(文中的回复是replies
,理解为Redis
服务端会一次性返回一个批量回复结果)。- 当
Redis
客户端订阅Pub/Sub
信道时,该协议会更改语义并成为推送协议(push protocol
),也就是说,客户端不再需要发送命令,因为Redis
服务端将自动向客户端(订阅了改信道的客户端)发送新消息(这里的意思是:在订阅/发布模式下,消息是由Redis
服务端主动推送给订阅了特定信道的Redis
客户端)。
除了上述两个特例之外,Redis
协议是一种简单的请求-响应协议。
4. 数据类型
RESP
在Redis 1.2
中引入,在Redis 2.0
,RESP
正式成为与Redis
服务端通信的标准方案。也就是如果需要编写Redis
客户端,你就必须在客户端中实现此协议。
RESP
本质上是一种序列化协议,它支持的数据类型如下:单行字符串、错误消息、整型数字、定长字符串和RESP
数组。
RESP
在Redis
中用作请求-响应协议的方式如下:
Redis
客户端将命令封装为RESP
的数组类型(数组元素都是定长字符串类型,注意这一点,很重要)发送到Redis
服务器。Redis
服务端根据命令实现选择对应的RESP
数据类型之一进行回复。
在RESP
中,数据类型取决于数据报的第一个字节:
- 单行字符串的第一个字节为
+
。 - 错误消息的第一个字节为
-
。 - 整型数字的第一个字节为
:
。 - 定长字符串的第一个字节为
$
。 RESP
数组的第一个字节为*
。
另外,在RESP
中可以使用定长字符串或者数组的特殊变体来表示Null
值,后面会提及。在RESP
中,协议的不同部分始终以\r\n
(CRLF
)终止。
4.1. 简单字符串
简单字符串的编码方式如下:
- 第一个字节为
+
。 - 紧接着的是一个不能包含
CR
或者LF
字符的字符串。 - 以
CRLF
终止。
简单字符串能够保证在最小开销的前提下传输非二进制安全的字符串。例如很多Redis
命令执行成功后服务端需要回复OK
字符串,此时通过简单字符串编码为5字节的数据报如下:
+OK\r\n
如果需要发送二进制安全的字符串,那么需要使用定长字符串。
当Redis
服务端用简单字符串响应时,Redis
客户端库应该向调用者返回一个字符串,该响应到调用者的字符串由+
之后直到字符串内容末尾的字符组成,不包括最后的CRLF
字节。
4.2. 错误消息
错误消息类型是RESP
特定的数据类型。实际上,错误消息类型和简单字符串类型基本一致,只是其第一个字节为-
。错误消息类型跟简单字符串类型的最大区别是:错误消息作为Redis
服务端响应的时候,对于客户端而言应该感知为异常,而错误消息中的字符串内容应该感知为Redis
服务端返回的错误信息。错误消息的编码方式如下:
- 第一个字节为
-
。 - 紧接着的是一个不能包含
CR
或者LF
字符的字符串。 - 以
CRLF
终止。
一个简单的例子如下:
-Error message\r\n
Redis
服务端只有在真正发生错误或者感知错误的时候才会回复错误消息,例如尝试对错误的数据类型执行操作或者命令不存在等等。Redis
客户端接收到错误消息的时候,应该触发异常(一般情况就是直接抛出异常,可以根据错误消息的内容进行异常分类)。下面是错误消息响应的一些例子:
-ERR unknown command 'foobar'
-WRONGTYPE Operation against a key holding the wrong kind of value
-
之后的第一个单词到第一个空格或换行符之间的内容,代表返回的错误类型。这只是Redis
使用的约定,不是RESP
错误消息格式的一部分。
例如,ERR
是通用错误,WRONGTYPE
则是更具体的错误,表示客户端试图针对错误的数据类型执行操作。这种定义方式称为错误前缀,是一种使客户端能够理解服务器返回的错误类型的方法,而不必依赖于所给出的确切消息定义,该消息可能会随时间而变化。
客户端实现可以针对不同的错误类型返回不同种类的异常,或者可以通过将错误类型的名称作为字符串直接提供给调用方来提供捕获错误的通用方法。
但是,不应该将错误消息分类处理的功能视为至关重要的功能,因为它作用并不巨大,并且有些的客户端实现可能会简单地返回特定值去屏蔽错误消息作为通用的异常处理,例如直接返回false
。
4.3. 整型数字
整型数字的编码方式如下:
- 第一个字节为
:
。 - 紧接着的是一个不能包含
CR
或者LF
字符的字符串,也就是数字要先转换为字符序列,最终要输出为字节。 - 以
CRLF
终止。
例如:
:0\r\n
:1000\r\n
许多Redis
命令返回整型数字,像INCR
,LLEN
和LASTSAVE
命令等等。
返回的整型数字没有特殊的含义,像INCR
返回的是增量的总量,而LASTSAVE
是UNIX
时间戳。但是Redis
服务端保证返回的整型数字在带符号的64位整数范围内。
有些情况下,返回的整型数字会指代true
或者false
。如EXISTS
或者SISMEMBER
命令执行返回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
。
4.4. 定长字符串
定长字符串用于表示一个最大长度为512MB
的二进制安全的字符串(Bulk
,本身有体积大的含义)。定长字符串的编码方式如下:
- 第一个字节为
$
。 - 紧接着的是组成字符串的字节数长度(称为
prefixed length
,也就是前缀长度),前缀长度分块以CRLF
终止。 - 然后是一个不能包含
CR
或者LF
字符的字符串,也就是数字要先转换为字符序列,最终要输出为字节。 - 以
CRLF
终止。
举个例子,doge
使用定长字符串编码如下:
第一个字节 | 前缀长度 | CRLF |
字符串内容 | CRLF |
定长字符串 | |
---|---|---|---|---|---|---|
$ |
4 |
\r\n |
doge |
\r\n |
===> |
$4\r\ndoge\r\n |
foobar
使用定长字符串编码如下:
第一个字节 | 前缀长度 | CRLF |
字符串内容 | CRLF |
定长字符串 | |
---|---|---|---|---|---|---|
$ |
6 |
\r\n |
foobar |
\r\n |
===> |
$6\r\nfoobar\r\n |
表示空字符串(Empty String
,对应Java中的""
) 的时候,使用定长字符串编码如下:
第一个字节 | 前缀长度 | CRLF |
CRLF |
定长字符串 | |
---|---|---|---|---|---|
$ |
0 |
\r\n |
\r\n |
===> |
$0\r\n\r\n |
定长字符串也可以使用特殊的格式来表示Null
值,指代值不存在。在这种特殊格式中,前缀长度为-1,并且没有数据,因此使用定长字符串对Null
值进行编码如下:
第一个字节 | 前缀长度 | CRLF |
定长字符串 | |
---|---|---|---|---|
$ |
-1 |
\r\n |
===> |
$-1\r\n |
当Redis
服务端返回定长字符串编码的Null
值的时候,客户端不应该返回空字符串,而应该返回对应编程语言中的Null
对象。例如Ruby
中对应nil
,C
语言中对应NULL
,Java
中对应null
,以此类推。
4.5. 数组
Redis
客户端使用RESP
数组发送命令到Redis
服务端。与此相似,某些Redis
命令执行完毕后服务端需要使用RESP
数组类型将元素集合返回给客户端,如返回一个元素列表的LRANGE
命令。RESP
数组和我们认知中的数组并不完全一致,它的编码格式如下:
- 第一个字节为
*
。 - 紧接着的是组成
RESP
数组的元素个数(十进制数,但是最终需要转换为字节序列,如10需要转换为1
和0
两个相邻的字节),元素个数分块以CRLF
终止。 RESP
数组的每个元素内容,每个元素可以是任意的RESP
数据类型。
一个空的RESP
数组的编码如下:
*0\r\n
一个包含2个定长字符串元素内容分别为foo
和bar
的RESP
数组的编码如下:
*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
通用格式就是:*<count>CRLF
作为RESP
数组的前缀部分,而组成RESP
数组的其他数据类型的元素只是一个接一个地串联在一起。例如一个包含3个整数类型元素的RESP
数组的编码如下:
*3\r\n:1\r\n:2\r\n:3\r\n
RESP
数组的元素不一定是同一种数据类型,可以包含混合类型的元素。例如下面是一个包含4个整数类型元素和1个定长字符串类型元素(一共有5个元素)的RESP
数组的编码(为了看得更清楚,分多行进行编码,实际上不能这样做):
# 元素个数
*5\r\n
# 第1个整型类型的元素
:1\r\n
# 第2个整型类型的元素
:2\r\n
# 第3个整型类型的元素
:3\r\n
# 第4个整型类型的元素
:4\r\n
# 定长字符串类型的元素
$6\r\n
foobar\r\n
Redis
服务端响应报的首行*5\r\n
定义了后面会紧跟着5个回复数据,然后每个回复数据分别作元素项,构成了用于传输的多元素定长回复(Multi Bulk Reply
,感觉比较难翻译,这里的大概意思就是每个回复行都是整个回复报中的一个项)。
RESP
数组中也存在Null
值的概念,下面称为RESP Null Array
。处于历史原因,RESP
数组中采用了另一种特殊的编码格式定义Null
值,区别于定长字符串中的Null
值字符串。例如,BLPOP
命令执行超时的时候,就会返回一个RESP Null Array
类型的响应。RESP Null Array
的编码如下:
*-1\r\n
当Redis
服务端的回复是RESP Null Array
类型的时候,客户端应该返回一个Null
对象,而不是一个空数组或者空列表。这一点比较重要,它是区分回复是空数组(也就是命令正确执行完毕,返回结果正常)或者其他原因(如BLPOP
命令的超时等)的关键。
RESP
数组的元素也可以是RESP
数组,下面是一个包含2个RESP
数组类型的元素的RESP
数组,编码如下(为了看得更清楚,分多行进行编码,实际上不能这样做):
# 元素个数
*2\r\n
# 第1个RESP数组元素
*3\r\n
:1\r\n
:2\r\n
:3\r\n
# 第2个RESP数组元素
*2\r\n
+Foo\r\n
-Bar\r\n
上面的RESP
数组的包含2个RESP
数组类型的元素,第1个RESP
数组元素包含3个整型类型的元素,而第2个RESP
数组元素包含1个简单字符串类型的元素和1个错误消息类型的元素。
RESP
数组中的Null元素
RESP
数组中的单个元素也有Null
值的概念,下面称为Null
元素。Redis
服务端回复如果是RESP
数组类型,并且RESP
数组中存在Null
元素,那么意味着元素丢失,绝对不能用空字符串替代。缺少指定键的前提下,当与GET
模式选项一起使用时,SORT
命令可能会发生这种情况。
下面是一个包含Null
元素的RESP
数组的例子(为了看得更清楚,分多行进行编码,实际上不能这样做):
*3\r\n
$3\r\n
foo\r\n
$-1\r\n
$3\r\n
bar\r\n
RESP
数组中的第2个元素是Null
元素,客户端API
最终返回的内容应该是:
# Ruby
["foo",nil,"bar"]
# Java
["foo",null,"bar"]
5. 发送命令
格式
*<参数数量> CRLF
$<参数 1 的字节数量> CRLF
<参数 1 的数据> CRLF
...
$<参数 N 的字节数量> CRLF
<参数 N 的数据> CRLF
示例
*3
$3
SET
$5
mykey
$7
myvalue
上述格式是格式化显示的结果,实际传输的格式如下
"*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"
6. 回复命令
redis的返回结果类型分为五种
- 状态回复(status reply)的第一个字节是 “+”
- 错误回复(error reply)的第一个字节是 “-“
- 整数回复(integer reply)的第一个字节是 “:”
- 字符串回复(bulk reply)的第一个字节是 “$”
- 多条字符串回复(multi bulk reply)的第一个字节是 “*”
6.1 状态回复
一个状态回复(或者单行回复,single line reply)是一段以 “+” 开始、 “\r\n” 结尾的单行字符串。 示例:
+OK
客户端库应该返回 “+” 号之后的所有内容。 比如在在上面的这个例子中, 客户端就应该返回字符串 “OK” 。 状态回复通常由那些不需要返回数据的命令返回,这种回复不是二进制安全的,它也不能包含新行。状态回复的额外开销非常少,只需要三个字节(开头的 “+” 和结尾的 CRLF)。
6.2. 错误回复
错误回复和状态回复非常相似, 它们之间的唯一区别是, 错误回复的第一个字节是 “-“ , 而状态回复的第一个字节是 “+” 。 错误回复只在某些地方出现问题时发送: 比如说, 当用户对不正确的数据类型执行命令, 或者执行一个不存在的命令, 等等。一个客户端库应该在收到错误回复时产生一个异常。
示例:
-ERR unknown command 'foobar'
-WRONGTYPE Operation against a key holding the wrong kind of value
在 “-“ 之后,直到遇到第一个空格或新行为止,这中间的内容表示所返回错误的类型。
ERR 是一个通用错误,而 WRONGTYPE 则是一个更特定的错误。 一个客户端实现可以为不同类型的错误产生不同类型的异常, 或者提供一种通用的方式, 让调用者可以通过提供字符串形式的错误名来捕捉(trap)不同的错误。
6.3. 整数回复
整数回复就是一个以 “:” 开头, CRLF 结尾的字符串表示的整数。比如说, “:0\r\n” 和 “:1000\r\n” 都是整数回复。 返回整数回复的其中两个命令是 INCR 和 LASTSAVE 。 被返回的整数没有什么特殊的含义, INCR 返回键的一个自增后的整数值, 而 LASTSAVE 则返回一个 UNIX 时间戳, 返回值的唯一限制是这些数必须能够用 64 位有符号整数表示。 整数回复也被广泛地用于表示逻辑真和逻辑假: 比如 EXISTS 和 SISMEMBER 都用返回值 1 表示真, 0 表示假。 其他一些命令, 比如 SADD 、 SREM 和 SETNX , 只在操作真正被执行了的时候, 才返回 1 , 否则返回 0 。 以下命令都返回整数回复: SETNX 、 DEL 、 EXISTS 、 INCR 、 INCRBY 、 DECR 、 DECRBY 、 DBSIZE 、 LASTSAVE 、 RENAMENX 、 MOVE 、 LLEN 、 SADD 、 SREM 、 SISMEMBER 、 SCARD 。
6.4. 字符串回复/批量回复
服务器使用批量回复来返回二进制安全的字符串,字符串的最大长度为 512 MB 。服务器发送的内容中:
第一字节为 "$" 符号
接下来跟着的是表示实际回复长度的数字值
之后跟着一个 CRLF
再后面跟着的是实际回复数据
最末尾是另一个 CRLF
例如
客户端:GET mykey
服务器:foobar
对于这个的 GET 命令,服务器实际发送的内容为:
"$6\r\nfoobar\r\n"
如果被请求的值不存在, 那么批量回复会将特殊值 -1 用作回复的长度值, 就像这样:
客户端:GET non-existing-key
服务器:$-1
这种回复称为空批量回复(NULL Bulk Reply)。 当请求对象不存在时,客户端应该返回空对象,而不是空字符串。
6.5. 多条字符串回复/多条批量回复
像 LRANGE 这样的命令需要返回多个值, 这一目标可以通过多条批量回复来完成。 多条批量回复是由多个回复组成的数组, 数组中的每个元素都可以是任意类型的回复, 包括多条批量回复本身。 多条批量回复的第一个字节为 “*” , 后跟一个字符串表示的整数值, 这个值记录了多条批量回复所包含的回复数量, 再后面是一个 CRLF 。
客户端: LRANGE mylist 0 3
服务器: *4
服务器: $3
服务器: foo
服务器: $3
服务器: bar
服务器: $5
服务器: Hello
服务器: $5
服务器: World
在上面的示例中,服务器发送的所有字符串都由 CRLF 结尾。 正如你所见到的那样, 多条批量回复所使用的格式, 和客户端发送命令时使用的统一请求协议的格式一模一样。 它们之间的唯一区别是:
统一请求协议只发送批量回复。
而服务器应答命令时所发送的多条批量回复,则可以包含任意类型的回复。
以下例子展示了一个多条批量回复, 回复中包含四个整数值, 以及一个二进制安全字符串:
*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 条回复的正文。
多条批量回复也可以是空白的(empty), 就像这样:
客户端: LRANGE nokey 0 1 服务器: *0\r\n
无内容的多条批量回复(null multi bulk reply)也是存在的, 比如当 BLPOP 命令的阻塞时间超过最大时限时, 它就返回一个无内容的多条批量回复, 这个回复的计数值为 -1 :
客户端: BLPOP key 1 服务器: *-1\r\n
客户端库应该区别对待空白多条回复和无内容多条回复: 当 Redis 返回一个无内容多条回复时, 客户端库应该返回一个 null 对象, 而不是一个空数组。
6.6. 多条批量回复中的空元素
多条批量回复中的元素可以将自身的长度设置为 -1 , 从而表示该元素不存在, 并且也不是一个空白字符串(empty string)。 当 SORT 命令使用 GET pattern 选项对一个不存在的键进行操作时, 就会发生多条批量回复中带有空白元素的情况。 以下例子展示了一个包含空元素的多重批量回复:
服务器: *3
服务器: $3
服务器: foo
服务器: $-1
服务器: $3
服务器: bar
其中, 回复中的第二个元素为空。对于这个回复, 客户端库应该返回类似于这样的回复:
["foo", nil, "bar"]
7. 参考资料
http://redisdoc.com/topic/protocol.html
https://mp.weixin.qq.com/s/M58XRiuxmT6GV7QlFJTZZA