[译]redis协议规范

友情提示

我大学的时候英语6级没过,因此但凡懂点英语的同学,如果你进到此页面,尽量去阅读原文,链接在下方原文地址.最次也要对照着原文阅读,以免我出了什么差错(这是不可避免的),坑了别的小伙伴.

如果您发现任何翻译的有歧义的地方,欢迎评论或者发邮件至huyanshi2580@gmail.com

原文地址

本文翻译自redis协议规范.


以下内容为译文,标题《Redis Protocol specification》


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

RESP是以下几项内容互相妥协的产物:

  • 容易实现
  • 快速解析
  • 人类可读

RESP可以序列化不同的数据类型,比如整数,字符串,数组.还有用于表达错误的一个特殊类型.

请求是一个字符串数组,相当于命令的参数,从客户区发送到服务器进行执行,Redis根据命令响应不同的数据类型.

RESP是二进制安全的,不需要处理从一个进程传输到另一个进程的批量数据,因为它使用前缀长度来传输批量数据。

注意:此处概述的协议仅用于客户端 - 服务器通信。Redis Cluster使用不同的二进制协议,以便在节点之间交换消息。

网络层

客户端连接到Redis服务器,创建到端口6379的TCP连接。

虽然RESP在技术上是非TCP特定的,但在Redis的上下文中,协议仅用于TCP连接(或类似的面向流的连接,如Unix套接字)。

请求-响应模型

Redis接受由不同参数组成的命令。收到命令后,将对其进行处理,并将回复发送回客户端。

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

  • Redis支持流水线操作(本文后面会介绍)。因此,客户端可以一次发送多个命令,并等待稍后的回复。
  • 当Redis客户端订阅Pub / Sub通道时,协议会更改语义并成为推送协议,也就是说,客户端不再需要发送命令,因为服务器会自动向客户端发送他收到的最新消息(客户端订阅的通道中)

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

RESP协议说明

RESP协议是在Redis 1.2中引入的,但它成为了与Redis 2.0中的Redis服务器通信的标准方式。这是您应该在Redis客户端中实现的协议。

RESP实际上是一个支持以下数据类型的序列化协议:简单字符串,错误,整数,批量字符串和数组。

RESP在Redis中用作请求 - 响应协议的方式如下:

  • 客户端将命令作为Bulk Strings的RESP数组发送到Redis服务器。
  • 服务器根据命令实现回复一种RESP类型。

在RESP中,一些数据的类型依赖于第一个字节:

  • 对于Simple Strings,回复的第一个字节是“+”
  • 对于错误,回复的第一个字节是“ - ”
  • 对于整数,回复的第一个字节是“:”
  • 对于Bulk Strings,回复的第一个字节是“$”
  • 对于数组,回复的第一个字节是“*”

此外,RESP能够使用稍后指定的Bulk Strings或Array的特殊变体来表示Null值。

在RESP中,协议的不同部分始终以“\r\n”(CRLF)结束。

RESP简单字符串

简单字符串按以下方式编码:加号字符,后跟不能包含CR或LF字符的字符串(不允许换行),由CRLF终止(即“\ r \ n”)。

Simple Strings用于以最小的开销传输非二进制安全字符串。例如,许多Redis命令在成功时仅回复“OK”,因为RESP Simple String使用以下5个字节进行编码:

1
"+OK\r\n"

为了发送二进制安全的字符串,使用RESP 批量字符串来代替.

当Redis使用Simple String回复时,客户端库应该向调用者返回一个字符串,该字符串由’+’之后的第一个字符组成,直到字符串结尾,不包括最终的CRLF字节。

RESP 错误

RESP具有特定的错误数据类型。实际上错误与RESP Simple Strings完全相同,但第一个字符是’ - ‘字符而不是加号。RESP中简单字符串和错误之间的真正区别在于客户端将错误视为异常,组成错误类型的字符串是错误消息本身。

基本格式是:

1
"-Error message\r\n"

错误回复仅在发生错误时发送,例如,如果您尝试对错误的数据类型执行操作,或者命令不存在等等。收到错误答复时,库客户端应引发异常。

以下是错误回复的示例:

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

“ - ”之后的第一个单词,直到第一个空格或换行符,表示返回的错误类型。这只是Redis使用的约定,不是RESP错误格式的一部分。

例如,ERR是一般错误,而WRONGTYPE更具体的错误意味着客户端尝试对错误的数据类型执行操作。这称为错误前缀,是一种允许客户端理解服务器返回的错误类型的方法,而不依赖于给定的确切消息,这可能随时间而变化。

客户端实现可以针对不同的错误返回不同类型的异常,或者可以通过直接将错误名称作为字符串提供给调用者来提供捕获错误的通用方法。

但是,这样的特性不应该被认为是至关重要的,因为它很少有用,并且有限的客户端实现可能只是返回一般的错误条件,例如false。

RESP整数

此类型只是一个CRLF终止的字符串,表示一个以“:”字节为前缀的整数。例如“:0 \ r \ n”或“:1000 \ r \ n”是整数回复。

许多Redis命令返回RESP整数,像INCR,LLEN和LASTSAVE。

返回的整数没有特殊含义,它只是INCR的增量数,LASTSAVE的UNIX时间等等。但是,返回的整数保证在有符号的64位整数范围内。

整数回复也被广泛使用以返回真或假。例如,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。

RESP Bulk Strings

Bulk Strings用于表示长度最大为512 MB的单个二进制安全字符串。

批量字符串按以下方式编码:

  • 一个“$”字节后跟组成字符串的字节数(一个前缀长度),由CRLF终止。
  • 实际的字符串数据。
  • 最终的CRLF。

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

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

空字符串仅仅是:

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

RESP Bulk Strings也可用于使用用于表示Null值的特殊格式来表示值的不存在。在这种特殊格式中,长度为-1,并且没有数据,因此Null表示为:

1
"$-1\r\n"

这称为Null Bulk String。

当服务器使用Null Bulk String回复时,客户端库API不应返回空字符串,而应返回nil对象。例如,Ruby库应返回’nil’,而C库应返回NULL(或在reply对象中设置特殊标志),依此类推。

RESP Arrays

客户端使用RESP Arrays将命令发送到Redis服务器。类似地,某些Redis命令将元素集合返回给客户端使用RESP Arrays作为返回类型。一个例子是LRANGE命令,它返回列表的元素。

RESP数组使用以下格式发送:

  • 一个*字符作为第一个字节,后面跟数组中的元素数量.紧跟CRLF.
  • Array的每个元素.

所以空数组如下:

1
"*0\r\n"

所以两个批量字符串”foo”和”bar”的数组编码为:

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

就像在*<count>CRLF之后看到的那样,数组中的其他数据类型一个挨着一个链接起来,例如三个整数的数组编码如下:

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

数组可以包含混合类型,元素不必是相同类型。例如,四个整数和批量字符串的列表可以编码如下:

1
2
3
4
5
6
7
*5\r\n
:1\r\n
:2\r\n
:3\r\n
:4\r\n
$6\r\n
foobar\r\n

(为清楚起见,答复分为多行)。

服务器发送的第一行是*5\r\n为了指定将跟随五个回复。然后发送构成多重回复项目的每个回复。

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

例如,当BLPOP命令超时时,它返回一个Null数组,其计数-1如下例所示:

1
"*-1\r\n"

当Redis使用Null数组回复时,客户端库API应返回空对象而不是空数组。这是区分空列表和不同条件(例如BLPOP命令的超时条件)所必需的。

RESP中可以使用数组的数组。例如,两个数组的数组编码如下:

1
2
3
4
5
6
7
8
*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数据类型编码一个由两个元素组成的数组,该数组包含三个整数1,2,3以及一个简单字符串和一个错误的数组。

数组中的空元素

Array的单个元素可以是Null。这在Redis回复中使用,以表示缺少此元素而不是空字符串。当缺少指定的键时,与GET 模式选项一起使用时,SORT命令可能会发生这种情况。包含Null元素的Array回复示例:

1
2
3
4
5
6
*3\r\n
$3\r\n
foo\r\n
$-1\r\n
$3\r\n
bar\r\n

第二个元素是Null。客户端库应返回如下内容:

1
["foo",nil,"bar"]

请注意,这不是前面部分中所述的例外,而只是进一步指定协议的示例。

发送命令到Redis服务器

既然您熟悉RESP序列化格式,那么编写Redis客户端库的实现将很容易。我们可以进一步指定客户端和服务器之间的交互如何工作:

  • 客户端向Redis服务器发送一个仅由Bulk Strings组成的RESP阵列。
  • Redis服务器回复发送任何有效RESP数据类型作为回复的客户端。
    因此,例如,典型的交互可以是下面这样。

客户端发送命令LLEN mylist以获取存储在密钥mylist中的列表长度,服务器回复一个Integer回复,如下例所示(C:是客户端,S:服务器)。

1
2
3
4
5
6
7
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作为一个整体发送。

多个命令和流水线操作

客户端可以使用相同的连接来发出多个命令。支持流水线操作,因此客户端可以通过单个写入操作发送多个命令,而无需在发出下一个命令之前读取上一个命令的服务器回复。所有的回复都可以在最后阅读。

有关更多信息,请查看我们关于Pipelining的页面。

内联命令

有时您只telnet需要掌握,并且需要向Redis服务器发送命令。虽然Redis协议易于实现,但在交互式会话中使用并不理想,并且redis-cli可能并不总是可用。因此,Redis还以一种专为人类设计的特殊方式接受命令,并称为内联命令格式。

以下是使用内联命令进行服务器/客户端聊天的示例(服务器聊天以S:开头,客户端与C聊天:)

1
2
C: PING
S: +PONG

以下是返回整数的内联命令的另一个示例:

1
2
C: EXISTS somekey
S: :0

基本上,您只需在telnet会话中编写以空格分隔的参数。由于没有命令启动*,而是在统一请求协议中使用,因此Redis能够检测到这种情况并解析您的命令。

Redis协议的高性能解析器

虽然Redis协议非常易读且易于实现,但它可以用类似于二进制协议的性能来实现。

RESP使用前缀长度来传输批量数据,因此永远不需要扫描有效负载以查找特殊字符(例如,使用JSON),也不需要引用需要发送到服务器的有效负载。

可以使用对每个字符执行单个操作的代码处理批量和多个批量长度,同时扫描CR字符,如下面的C代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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一起跳过而不进行任何处理。然后,可以使用不以任何方式检查有效负载的单个读取操作来读取批量数据。最后,丢弃剩余的CR和LF字符而不进行任何处理。

虽然性能与二进制协议相当,但Redis协议在大多数非常高级语言中实现起来要简单得多,从而减少了客户端软件中的错误数量。

完。





ChangeLog

2019-06-13 完成

以上皆为个人所思所得,如有错误欢迎评论区指正。

欢迎转载,烦请署名并保留原文链接。

联系邮箱:huyanshi2580@gmail.com

更多学习笔记见个人博客——>呼延十