Redis 序列化协议规范
Redis 序列化协议 (RESP) 是客户端实现的线协议
为了与 Redis 服务器通信,Redis 客户端使用一种称为 Redis 序列化协议 (RESP) 的协议。虽然该协议是专门为 Redis 设计的,但您也可以将其用于其他客户端-服务器软件项目。
RESP 是对以下考虑因素的折衷
- 易于实现。
- 解析速度快。
- 人机可读。
RESP 可以序列化不同的数据类型,包括整数、字符串和数组。它还具有一个错误特定类型。客户端将请求作为字符串数组发送到 Redis 服务器。数组的内容是服务器应执行的命令及其参数。服务器的回复类型是特定于命令的。
RESP 是二进制安全的,并且使用前缀长度来传输大块数据,因此它不需要处理从一个进程到另一个进程传输的大块数据。
RESP 是您应该在 Redis 客户端中实现的协议。
RESP 版本
对 RESP 协议第一个版本的支持是在 Redis 1.2 中引入的。在 Redis 1.2 中使用 RESP 是可选的,主要用于解决协议中的问题。
在 Redis 2.0 中,协议的下一个版本,即 RESP2,成为客户端与 Redis 服务器的标准通信方法。
RESP3 是 RESP2 的超集,其主要目标是简化客户端作者的工作。Redis 6.0 引入了对 RESP3 特性(不包括流式字符串和流式聚合)的实验性选择加入支持。此外,引入了 HELLO
命令,允许客户端进行握手并升级连接的协议版本(参见 客户端握手)。
在 Redis 7 及其之前版本中,RESP2 和 RESP3 客户端都可以调用所有核心命令。但是,对于不同的协议版本,命令可能会返回不同类型的回复。
未来版本的 Redis 可能会改变默认的协议版本,但 RESP2 不太可能完全被弃用。但是,即将发布的版本中的新功能可能需要使用 RESP3。
网络层
客户端通过建立到其端口的 TCP 连接(默认端口为 6379)来连接到 Redis 服务器。
虽然 RESP 在技术上不是特定于 TCP 的,但在 Redis 的上下文中,该协议仅与 TCP 连接(或类似的基于流的连接,例如 Unix 套接字)一起使用。
请求-响应模型
Redis 服务器接受由不同参数组成的命令。然后,服务器处理命令并将回复发送回客户端。
这是最简单的模型;但是,也有一些例外
- Redis 请求可以进行 流水线处理。流水线处理使客户端能够一次发送多个命令,并在稍后等待回复。
- 当 RESP2 连接订阅 发布/订阅 通道时,协议的语义会发生变化,并成为推送协议。客户端不再需要发送命令,因为服务器会在收到新消息后立即自动将新消息发送给客户端(对于客户端订阅的通道)。
MONITOR
命令。调用MONITOR
命令会将连接切换到临时的推送模式。此模式的协议未指定,但易于解析。- 保护模式。从非回环地址到处于保护模式的 Redis 的连接会被服务器拒绝并终止。在终止连接之前,Redis 无条件地发送
-DENIED
回复,无论客户端是否写入套接字。 - RESP3 推送类型。顾名思义,推送类型允许服务器将带外数据发送到连接。服务器可以在任何时间推送数据,并且数据不一定与客户端执行的特定命令相关。
除了这些例外,Redis 协议是一个简单的请求-响应协议。
RESP 协议描述
RESP 本质上是一种序列化协议,支持多种数据类型。在 RESP 中,数据的第一个字节确定其类型。
Redis 通常以以下方式使用 RESP 作为 请求-响应 协议
- 客户端将命令作为 数组 的 大块字符串 发送到 Redis 服务器。数组中的第一个(有时也是第二个)大块字符串是命令的名称。数组的后续元素是命令的参数。
- 服务器用 RESP 类型进行回复。回复的类型由命令的实现决定,也可能由客户端的协议版本决定。
RESP 是一种二进制协议,使用以标准 ASCII 编码的控制序列。例如,A
字符使用值为 65 的二进制字节进行编码。同样,字符 CR (\r
)、LF (\n
) 和 SP (
) 分别具有 13、10 和 32 的二进制字节值。
\r\n
(CRLF) 是协议的终止符,它始终将协议的各个部分隔开。
RESP 序列化有效负载中的第一个字节始终标识其类型。后续字节构成类型的內容。
我们将每种 RESP 数据类型归类为简单、大块或聚合。
简单类型类似于编程语言中表示纯文本值的标量。布尔值和整数就是这样的示例。
RESP 字符串可以是简单或大块。简单字符串永远不包含回车符 (\r
) 或换行符 (\n
) 字符。大块字符串可以包含任何二进制数据,也可以称为二进制或blob。请注意,大块字符串可以由客户端进一步编码和解码,例如使用宽多字节编码。
聚合,例如数组和映射,可以具有不同数量的子元素和嵌套级别。
下表总结了 Redis 支持的 RESP 数据类型
RESP 数据类型 | 最小协议版本 | 类别 | 第一个字节 |
---|---|---|---|
简单字符串 | RESP2 | 简单 | + |
简单错误 | RESP2 | 简单 | - |
整数 | RESP2 | 简单 | : |
大块字符串 | RESP2 | 聚合 | $ |
数组 | RESP2 | 聚合 | * |
空值 | RESP3 | 简单 | _ |
布尔值 | RESP3 | 简单 | # |
双精度浮点数 | RESP3 | 简单 | , |
大数 | RESP3 | 简单 | ( |
大块错误 | RESP3 | 聚合 | ! |
逐字字符串 | RESP3 | 聚合 | = |
映射 | RESP3 | 聚合 | % |
集合 | RESP3 | 聚合 | ~ |
推送 | RESP3 | 聚合 | > |
简单字符串
简单字符串被编码为一个加号 (+
) 字符,后面跟着一个字符串。该字符串不能包含 CR (\r
) 或 LF (\n
) 字符,并以 CRLF (即,\r\n
) 终止。
简单字符串以最小的开销传输短的非二进制字符串。例如,许多 Redis 命令在成功时只回复“OK”。此简单字符串的编码如下 5 个字节
+OK\r\n
当 Redis 回复一个简单字符串时,客户端库应将由 +
后的第一个字符到字符串末尾(不包括最后的 CRLF 字节)组成的字符串值返回给调用方。
要发送二进制字符串,请使用 大块字符串。
简单错误
RESP 具有专门用于错误的数据类型。简单错误,或者简称为错误,类似于 简单字符串,但它们的第一个字符是减号 (-
) 字符。RESP 中简单字符串和错误之间的区别在于,客户端应将错误视为异常,而错误类型中编码的字符串是错误消息本身。
基本格式如下
-Error message\r\n
只有在出现错误时,Redis 才会回复错误,例如,当您尝试对错误的数据类型进行操作时,或者当命令不存在时。当客户端接收到错误回复时,它应引发异常。
以下是错误回复的示例
-ERR unknown command 'asdf'
-WRONGTYPE Operation against a key holding the wrong kind of value
-
后的第一个大写单词(直到第一个空格或换行符)表示返回的错误类型。此单词称为错误前缀。请注意,错误前缀是 Redis 使用的约定,而不是 RESP 错误类型的一部分。
例如,在 Redis 中,ERR
是一个通用错误,而 WRONGTYPE
是一个更具体的错误,它意味着客户端尝试对错误的数据类型执行操作。错误前缀允许客户端了解服务器返回的错误类型,而无需检查确切的错误消息。
客户端实现可以针对各种错误返回不同类型的异常,或者通过直接将错误名称作为字符串提供给调用方来提供一种通用方式来捕获错误。
但是,此功能不应该被认为是至关重要的,因为它很少有用。此外,更简单的客户端实现可以返回一个通用的错误值,例如 false
。
整数
此类型是一个 CRLF 终止的字符串,表示一个有符号的、以十进制表示的 64 位整数。
RESP 以以下方式对整数进行编码
:[<+|->]<value>\r\n
- 第一个字节是冒号 (
:
)。 - 可选的加号 (
+
) 或减号 (-
) 作为符号。 - 一个或多个十进制数字 (
0
..9
) 作为整数的无符号十进制值。 - CRLF 终止符。
例如,:0\r\n
和 :1000\r\n
是整数回复(分别为零和一千)。
许多 Redis 命令返回 RESP 整数,包括 INCR
、LLEN
和 LASTSAVE
。整数本身没有特殊意义,只有在返回它的命令的上下文中才有意义。例如,它是 INCR
的增量数字,它是 LASTSAVE
的 UNIX 时间戳等等。但是,返回的整数保证在有符号 64 位整数的范围内。
在某些情况下,整数可以表示真假布尔值。例如,SISMEMBER
对真返回 1,对假返回 0。
其他命令,包括 SADD
、SREM
和 SETNX
,在数据更改时返回 1,否则返回 0。
大块字符串
大块字符串表示单个二进制字符串。该字符串可以是任何大小,但默认情况下,Redis 将其限制为 512 MB(参见 proto-max-bulk-len
配置指令)。
RESP 以以下方式对大块字符串进行编码
$<length>\r\n<data>\r\n
- 第一个字节是美元符号 (
$
)。 - 一个或多个十进制数字 (
0
..9
) 作为字符串的长度(以字节为单位),表示为无符号的十进制值。 - CRLF 终止符。
- 数据。
- 最后的 CRLF。
因此,字符串“hello”被编码如下
$5\r\nhello\r\n
空字符串的编码为
$0\r\n\r\n
空大块字符串
虽然 RESP3 具有专门用于 空值 的数据类型,但 RESP2 没有此类类型。相反,由于历史原因,RESP2 中空值的表示通过 大块字符串 和 数组 类型的预定义形式实现。
空大块字符串表示不存在的值。当目标键不存在时,GET
命令返回空大块字符串。
它被编码为长度为负一 (-1) 的大块字符串,如下所示
$-1\r\n
当服务器用空大块字符串回复时,Redis 客户端应返回一个空对象,而不是返回空字符串。例如,Ruby 库应返回 nil
,而 C 库应返回 NULL
(或在回复对象中设置一个特殊标志)。
数组
客户端将命令作为 RESP 数组发送到 Redis 服务器。同样,一些返回元素集合的 Redis 命令使用数组作为其回复。例如,LRANGE
命令返回列表的元素。
RESP 数组的编码使用以下格式
*<number-of-elements>\r\n<element-1>...<element-n>
- 第一个字节是星号 (
*
)。 - 一个或多个十进制数字 (
0
..9
) 作为数组中元素的数量,表示为无符号的十进制值。 - CRLF 终止符。
- 为数组的每个元素添加一个额外的 RESP 类型。
因此,空数组只是以下内容
*0\r\n
而包含两个大块字符串“hello”和“world”的数组的编码为
*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n
如您所见,在数组前缀的 *<count>CRLF
部分之后,构成数组的其他数据类型一个接一个地连接起来。例如,三个整数的数组被编码如下
*3\r\n:1\r\n:2\r\n:3\r\n
数组可以包含混合数据类型。例如,以下编码是一个包含四个整数和一个批量字符串的列表。
*5\r\n
:1\r\n
:2\r\n
:3\r\n
:4\r\n
$5\r\n
hello\r\n
(原始 RESP 编码为了可读性被拆分为多行)。
服务器发送的第一行是 *5\r\n
。这个数字值告诉客户端将要发送五个回复类型。然后,每个后续回复构成数组中的一个元素。
所有聚合 RESP 类型都支持嵌套。例如,一个包含两个数组的嵌套数组被编码如下
*2\r\n
*3\r\n
:1\r\n
:2\r\n
:3\r\n
*2\r\n
+Hello\r\n
-World\r\n
(原始 RESP 编码为了可读性被拆分为多行)。
以上编码了一个包含两个元素的数组。第一个元素是一个数组,它依次包含三个整数 (1, 2, 3)。第二个元素是另一个数组,包含一个简单字符串和一个错误。
空数组
尽管 RESP3 针对 空值 有专门的数据类型,但 RESP2 没有这种类型。相反,由于历史原因,RESP2 中空值的表示是通过 批量字符串 和 数组 类型的预定形式来实现的。
空数组作为表示空值的一种替代方式存在。例如,当 BLPOP
命令超时时,它会返回一个空数组。
空数组的编码是一个长度为 -1 的数组的编码,即
*-1\r\n
当 Redis 返回一个空数组时,客户端应该返回一个空对象而不是一个空数组。这是为了区分空列表和不同的条件(例如,BLPOP
命令的超时条件)。
数组中的空元素
数组的单个元素可能是 空批量字符串。这在 Redis 回复中用于表明这些元素是缺失的,而不是空字符串。例如,这可能发生在使用 GET pattern
选项时使用 SORT
命令,如果指定的键缺失。
以下是一个包含空元素的数组回复的示例
*3\r\n
$5\r\n
hello\r\n
$-1\r\n
$5\r\n
world\r\n
在上面,第二个元素是空的。客户端库应该将其调用者返回如下内容
["hello",nil,"world"]
空值
空数据类型表示不存在的值。
空的编码是下划线 (_
) 字符,后跟 CRLF 终止符 (\r\n
)。以下为空的原始 RESP 编码
_\r\n
由于历史原因,RESP2 有两个专门设计的用于表示批量字符串和数组的空值的。这种二元性一直是一种冗余,它没有为协议本身增加任何语义价值。
RESP3 中引入的空类型旨在解决这一问题。
布尔值
RESP 布尔值按如下方式编码
#<t|f>\r\n
- 第一个字节为井号字符 (
#
)。 - 对于真值,为
t
字符,对于假值,为f
字符。 - CRLF 终止符。
双精度浮点数
Double RESP 类型编码一个双精度浮点数。Double 的编码方式如下
,[<+|->]<integral>[.<fractional>][<E|e>[sign]<exponent>]\r\n
- 第一个字节为逗号字符 (
,
)。 - 可选的加号 (
+
) 或减号 (-
) 作为符号。 - 一个或多个十进制数字 (
0
..9
) 作为无符号的十进制整数。 - 可选的点 (
.
),后跟一个或多个十进制数字 (0
..9
) 作为无符号的十进制分数。 - 可选的大写或小写字母 E (
E
或e
),后跟可选的加号 (+
) 或减号 (-
) 作为指数的符号,以一个或多个十进制数字 (0
..9
) 作为无符号的十进制指数结束。 - CRLF 终止符。
以下是数字 1.23 的编码
,1.23\r\n
由于小数部分是可选的,因此十 (10) 的整数值可以通过 RESP 编码为整数和 double。
:10\r\n
,10\r\n
在这种情况下,Redis 客户端应该分别返回本机整数和 double 值,前提是这些类型受其实现语言的支持。
正无穷大、负无穷大和 NaN 值的编码方式如下
,inf\r\n
,-inf\r\n
,nan\r\n
大数
此类型可以编码超出带符号 64 位整数范围的整数值。
大数使用以下编码
([+|-]<number>\r\n
- 第一个字节为左括号字符 (
(
)。 - 可选的加号 (
+
) 或减号 (-
) 作为符号。 - 一个或多个十进制数字 (
0
..9
) 作为无符号的十进制值。 - CRLF 终止符。
示例
(3492890328409238509324850943850943825024385\r\n
大数可以是正数或负数,但不能包含小数。用具有大数类型的语言编写的客户端库应该返回一个大数。当不支持大数时,客户端应该返回一个字符串,并在可能的情况下向调用者发出信号表明回复是一个大整数(取决于客户端库使用的 API)。
大块错误
此类型将 简单错误 的目的与 批量字符串 的表达能力相结合。
它的编码方式为
!<length>\r\n<error>\r\n
- 第一个字节为感叹号 (
!
)。 - 一个或多个十进制数字 (
0
..9
) 作为错误的长度(以字节为单位),作为无符号的十进制值。 - CRLF 终止符。
- 错误本身。
- 最后的 CRLF。
按照惯例,错误以一个大写(以空格分隔)的词开头,该词传达了错误消息。
例如,错误 "SYNTAX invalid syntax" 由以下协议编码表示
!21\r\n
SYNTAX invalid syntax\r\n
(原始 RESP 编码为了可读性被拆分为多行)。
逐字字符串
逐字字符串的 RESP 编码方式如下
=<length>\r\n<encoding>:<data>\r\n
- 第一个字节为等号 (
=
)。 - 一个或多个十进制数字 (
0
..9
) 作为字符串的总长度(以字节为单位),作为无符号的十进制值。 - CRLF 终止符。
- 正好三个 (3) 字节表示数据的编码。
- 冒号 (
:
) 字符分隔编码和数据。 - 数据。
- 最后的 CRLF。
示例
=15\r\n
txt:Some string\r\n
(原始 RESP 编码为了可读性被拆分为多行)。
一些客户端库可能会忽略此类型和字符串类型之间的差异,并在两种情况下都返回本机字符串。但是,交互式客户端(例如命令行界面(例如,redis-cli
))可以使用此类型,并知道他们的输出应该原样显示给人类用户,而无需引用字符串。
例如,Redis 命令 INFO
输出一个包含换行符的报告。当使用 RESP3 时,redis-cli
正确地显示它,因为它被发送为逐字字符串回复(其三个字节为 "txt")。但是,当使用 RESP2 时,redis-cli
被硬编码为查找 INFO
命令以确保其正确显示给用户。
映射
RESP 映射编码了键值对的集合,即字典或哈希。
它的编码方式为
%<number-of-entries>\r\n<key-1><value-1>...<key-n><value-n>
- 第一个字节为百分号字符 (
%
)。 - 一个或多个十进制数字 (
0
..9
) 作为映射中的条目数或键值对数,作为无符号的十进制值。 - CRLF 终止符。
- 映射中每个键和值有两个额外的 RESP 类型。
例如,以下 JSON 对象
{
"first": 1,
"second": 2
}
可以在 RESP 中编码如下
%2\r\n
+first\r\n
:1\r\n
+second\r\n
:2\r\n
(原始 RESP 编码为了可读性被拆分为多行)。
映射键和值都可以是 RESP 中的任何类型。
Redis 客户端应该返回其语言提供的惯用字典类型。但是,低级编程语言(例如 C)可能会返回一个数组以及类型信息,这些信息向调用者表明它是一个字典。
key1, value1, key2, value2, ...
。集合
集合有点像 数组,但它们是无序的,并且应该只包含唯一元素。
RESP 集合的编码为
~<number-of-elements>\r\n<element-1>...<element-n>
- 第一个字节为波浪号 (
~
)。 - 一个或多个十进制数字 (
0
..9
) 作为集合中元素的数量,作为无符号的十进制值。 - CRLF 终止符。
- 集合中的每个元素都有一个额外的 RESP 类型。
如果客户端的编程语言中提供了本机集合类型,则客户端应该返回本机集合类型。或者,如果不存在本机集合类型,则可以使用一个数组以及类型信息(例如在 C 中)。
推送
RESP 的推送包含带外数据。它们是协议请求-响应模型的例外,并为连接提供了一个通用的 推送模式。
推送事件的编码类似于 数组,只是它们的第一个字节不同
><number-of-elements>\r\n<element-1>...<element-n>
- 第一个字节为大于号 (
>
)。 - 一个或多个十进制数字 (
0
..9
) 作为消息中元素的数量,作为无符号的十进制值。 - CRLF 终止符。
- 推送事件中的每个元素都有一个额外的 RESP 类型。
推送数据可能位于任何 RESP 数据类型的前面或后面,但永远不会在其中。这意味着客户端不会在映射回复的中间找到推送数据,例如。这也意味着推送数据可能出现在命令回复之前或之后,以及单独出现(无需调用任何命令)。
客户端应该通过调用一个回调函数来响应推送,该回调函数实现了对推送数据的处理。
客户端握手
新的 RESP 连接应该通过调用 HELLO
命令来开始会话。这种做法可以实现两件事
- 它允许服务器向后兼容 RESP2 版本。这在 Redis 中是必要的,以便使协议向版本 3 的过渡更加平滑。
- The
HELLO
命令返回有关服务器和客户端可用于不同目标的协议的信息。
The HELLO
命令具有以下高级语法
HELLO <protocol-version> [optional-arguments]
命令的第一个参数是我们希望连接设置为的协议版本。默认情况下,连接以 RESP2 模式启动。如果我们指定一个过大的版本,并且服务器不支持该版本,它应该回复 -NOPROTO
错误。示例
Client: HELLO 4
Server: -NOPROTO sorry, this protocol version is not supported.
此时,客户端可以重试使用更低的协议版本。
类似地,客户端可以轻松地检测到只能使用 RESP2 的服务器
Client: HELLO 3
Server: -ERR unknown command 'HELLO'
然后,客户端可以继续使用 RESP2 与服务器通信。
请注意,即使支持协议版本,HELLO
命令也可能返回错误,不执行任何操作并保持 RESP2 模式。例如,当与命令的可选 AUTH
子句中的无效身份验证凭据一起使用时
Client: HELLO 3 AUTH default mypassword
Server: -ERR invalid password
(the connection remains in RESP2 mode)
对 HELLO
命令的成功回复是一个映射回复。回复中的信息部分取决于服务器,但某些字段对于所有 RESP3 实现都是必需的
- server: "redis"(或其他软件名称)。
- version: 服务器的版本。
- proto: 支持的最高 RESP 协议版本。
在 Redis 的 RESP3 实现中,还发出以下字段
- id: 连接的标识符 (ID)。
- mode: "standalone"、"sentinel" 或 "cluster"。
- role: "master" 或 "replica"。
- modules: 已加载模块的列表,作为批量字符串的数组。
向 Redis 服务器发送命令
现在您已经熟悉了 RESP 序列化格式,您可以使用它来帮助编写 Redis 客户端库。我们可以进一步详细说明客户端和服务器之间的交互方式。
- 客户端向 Redis 服务器发送一个仅包含批量字符串的 数组。
- Redis 服务器回复客户端,发送任何有效的 RESP 数据类型作为回复。
例如,典型的交互可能是以下情况。
客户端发送命令 LLEN mylist
以获取存储在键 mylist 中的列表长度。然后服务器使用以下示例中所示的 整数 回复(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 服务器发送命令,但只有 telnet
可用。虽然 Redis 协议易于实现,但它并不适合交互式会话,而且 redis-cli
并不总是可用。出于这个原因,Redis 还接受内联命令格式的命令。
以下示例演示了使用内联命令的服务器/客户端交换(服务器聊天以 S:
开头,客户端聊天以 C:
开头)
C: PING
S: +PONG
以下是在服务器返回整数的内联命令的另一个示例
C: EXISTS somekey
S: :0
基本上,要发出内联命令,您可以在 telnet 会话中写入空格分隔的参数。由于没有命令以 *
(RESP 数组的标识字节)开头,Redis 会检测到这种情况并内联解析您的命令。
Redis 协议的高性能解析器
虽然 Redis 协议是人类可读的并且易于实现,但它的实现可以表现出类似于二进制协议的性能。
RESP 使用前缀长度来传输大量数据。这使得不需要扫描有效负载以查找特殊字符(例如,与解析 JSON 相比)。出于同样的原因,不需要对有效负载进行引用和转义。
读取聚合类型(例如,批量字符串或数组)的长度可以使用每字符执行一次操作的代码进行处理,同时扫描 CR 字符。
示例(在 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 一起跳过,而无需进一步处理。然后,可以使用单个读取操作读取批量数据,该操作不会以任何方式检查有效负载。最后,剩余的 CR 和 LF 字符将被丢弃,无需额外处理。
虽然 Redis 协议在性能方面与二进制协议相当,但它在大多数高级语言中更容易实现,从而减少了客户端软件中的错误数量。
Redis 客户端作者的提示
- 为了测试目的,使用 Lua 的类型转换 使 Redis 回复任何所需的 RESP2/RESP3。例如,RESP3 双精度浮点数可以这样生成
EVAL "return { double = tonumber(ARGV[1]) }" 0 1e0