*Redis 序列化协议规范
为了与 Redis 服务器通信,Redis 客户端使用一种称为 Redis 序列化协议(RESP)的协议。尽管该协议专门为 Redis 设计,但您也可以将其用于其他客户端-服务器软件项目。
RESP 是下面条件的折中:
- 实现起来简单。
- 解析速度快。
- 有可读性。
RESP 能够序列化不同的数据类型,包括整数、字符串和数组。它还具有特定错误类型。客户端将请求以字符串数组的形式发送到 Redis 服务器。数组的内容是服务器应执行的命令及其参数。 服务器的回复类型取决于具体的命令。
RESP 是二进制安全的,并使用前缀长度传输块数据,因此不需要处理从一个进程传输到另一个进程的块数据。 Redis 客户端中需实现此协议。
注意: 此处概述的协议仅用于客户端-服务器通信。Redis 集群用于节点之间交换消息时使用不同的二进制协议。
*RESP 版本
Redis 1.2 引入了第一个版本的 RESP 协议。
在 Redis 2.0 中,协议的下一个版本(即 RESP2)成为客户端与 Redis 服务器通信的标准方法。
RESP3 是 RESP2 的超集,主要目的是让客户端作者的工作更容易一些。Redis 6.0 引入了 RESP3 功能的实验性可选支持(不包括流字符串和流聚合)。 此外,HELLO 命令的引入允许客户端握手并升级连接的协议版本(参见 [客户端握手](#client-handshake)。
在 Redis 7 及以下版本中, RESP2 和 RESP3 客户端都可以调用所有核心命令。然而,命令可能会根据协议版本返回不同类型的回复。
未来的 Redis 版本可能会更改默认协议版本,但不太可能弃用 RESP2。然而,未来版本中的新功能可能需要使用 RESP3。
*网络层
客户端通过创建一个 TCP 连接到 Redis 服务器的端口(默认是 6379)。虽然从技术上讲 RESP 与 TCP 无关,但在 Redis 的上下文中,该协议仅与 TCP 连接(或类似流连接,如 Unix 套接字)一起使用。
*请求-响应模型
Redis 服务器接受由不同参数组成的命令。然后,服务器处理命令并将回复发送回客户端。这是最简单的模型,但有一些例外:
- Redis 请求可以 pipelined。管线化允许客户端一次性发送多个命令并稍后再等待回复。
- 当一个 RESP2 连接订阅 Pub/Sub 通道时,协议更改语义并成为 推送 协议。客户端无需再发送命令,因为服务器会在接收到新消息时自动将消息发送给客户端(针对客户端订阅的通道)。
- 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 | 聚合 | > |
*简单字符串
简单字符串以加号 (+) 字符开头,后跟一个字符串。 该字符串不能包含回车 (\r) 或换行 (\n) 字符,并以 CRLF(即 \r\n)结尾。
简单字符串以最小开销传输短的、非二进制字符串。 例如,许多 Redis 命令在成功时仅返回 "OK"。 此简单字符串的编码是以下 5 个字节:
"+OK\r\n"
当 Redis 以简单字符串回复时,客户端库应向调用者返回由 +
后的第一个字符到字符串末尾(不包括最终的 CRLF 字节)组成的一个字符串值。
要发送二进制字符串,请改用 块字符串。
*RESP 错误
RESP 有专门用于表示错误的数据类型。
简单错误,或仅错误,与 简单字符串 类似,但其第一个字符是减号 (-
)。RESP 中错误与简单字符串的区别在于客户端应将错误视为异常,而错误类型中编码的字符串是错误消息本身。
基本格式如下:
"-Error message\r\n"
Redis 仅在出现问题时回复错误,例如尝试对错误数据类型的键执行操作,或命令不存在。客户端在收到错误回复时应引发异常。
以下是错误回复的一些示例:
-ERR unknown command 'foobar'
-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 位整数的范围内。
在某些情况下,整数可以表示布尔值的 true 和 false。例如,SISMEMBER 返回 1 表示 true,0 表示 false。
其他命令,包括 SADD、SREM 和 SETNX,在数据更改时返回 1,否则返回 0。
*块字符串
块字符串表示单个二进制字符串。字符串的大小可以是任意的,但 Redis 默认将其限制为 512 MB(参见 proto-max-bulk-len
配置指令)。
RESP 以以下方式编码块字符串:
- 第一个字节是美元符号 (
$
)。 - 一个或多个十进制数字 (
0
..9
) 作为字符串的长度(以字节为单位),作为无符号十进制值。 - CRLF 终止符。
- 数据。
- 最终的 CRLF。
所以,字符串 "foobar" 编码如下:
"$6\r\nfoobar\r\n"
空字符串编码格式:
"$0\r\n\r\n"
*空值块字符串
虽然 RESP3 有一个专门的 空值 数据类型,但 RESP2 没有此类类型。相反,由于历史原因,RESP2 中的空值表示为 块字符串 和 数组 类型的预定形式。
空值块字符串表示一个不存在的值。当目标键不存在时,GET 命令返回空值块字符串。
它编码为一个长度为负一 (-1) 的块字符串,如下所示:
$-1\r\n
Redis 客户端在服务器回复空值块字符串时应返回一个空对象,而不是空字符串。例如,Ruby 库应返回 nil
,而 C 库应返回 NULL
(或在回复对象中设置一个特殊标志)。
*RESP 数组
客户端将命令作为 RESP 数组发送到 Redis 服务器。同样,某些返回元素集合的 Redis 命令使用数组作为回复。例如,LRANGE 命令返回列表的元素。
RESP 数组的编码使用以下格式:
- 第一个字节是星号 (
*
)。 - 一个或多个十进制数字 (
0
..9
) 作为数组中元素的数量,作为无符号十进制值。 - CRLF 终止符。
- 数组中每个元素的附加 RESP 类型。
因此,空数组只是:
"*0\r\n"
有两个 RESP 块字符串"foo" 和"bar"元素的 RESP 数组 :
"*2\r\n$3\r\nfoo\r\n$3\r\nbar\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
$6\r\n
foobar\r\n
(原始的 RESP 编码为提高可读性分为多行。)
所有聚合的 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)。第二个元素是另一个包含简单字符串和错误的数组。
{{% alert title="多块回复" color="info" %}} 在某些地方,RESP 数组类型可能被称为 多块。两者是相同的。 {{% /alert %}}
服务器发送的第一行是 *5\r\n
。此数值告诉客户端接下来有五个回复类型。然后,每个后续回复构成数组的一个元素。
*空值数组
虽然 RESP3 有一个专门的 空值 数据类型,但 RESP2 没有此类类型。相反,由于历史原因,RESP2 中的空值表示为 块字符串 和 数组 类型的预定形式。
空值数组的编码是长度为 -1 的数组,即:
*-1\r\n
当 Redis 以空数组回复时,客户端应返回空对象而不是空数组。这是必要的,以区分空列表和不同条件(例如 BLPOP 命令的超时条件)
*数组中的空元素
数组的单个元素可以是 空值块字符串。Redis 回复中使用此方法表示这些元素丢失而不是空字符串。例如,当 SORT 命令与 GET pattern
选项一起使用时,如果指定的键缺失,可能会出现这种情况。
以下是一个包含空值元素的数组回复示例:
*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
字符表示 true 值,f
字符表示 false 值。- CRLF 终止符。
*双精度数
双精度数编码表示双精度浮点值。双精度数以以下方式编码:
,[<+|->]<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)编码为整数和双精度数:
:10\r\n
,10\r\n
在这些情况下,在支持相应语言的客户端库中,应分别返回本机整数和双精度值。
正无穷大、负无穷大和 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)可能会返回一个数组,附加类型信息,指示调用者这是一个字典。
RESP2 没有映射类型。RESP2 中的映射表示为包含键和值的平铺数组。第一元素是键,紧随其后的是对应的值,依此类推,例如: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 版本。
- HELLO 命令返回关于服务器和协议的信息,客户端可以用来实现不同的目标。
HELLO 命令的高级语法如下:
HELLO <protocol-version> [可选参数]
第一个参数是希望连接使用的协议版本。默认情况下,连接以 RESP2 模式开始。如果指定的协议版本太高或不为服务器支持,则服务器应以 -NOPROTO
错误回复。例如:
客户端: HELLO 4
服务器: -NOPROTO 抱歉,此协议版本不受支持。
此时,客户端可能会尝试使用较低的协议版本。
类似地,客户端可以轻松检测仅支持 RESP2 的服务器:
客户端: HELLO 3
服务器: -ERR 未知命令 'HELLO'
然后,客户端可以使用 RESP2 与服务器通信。
请注意,即使协议版本受支持,HELLO 命令也可能会返回错误,而不执行任何操作并保持在 RESP2 模式。例如,当命令的可选 AUTH
子句中使用无效的身份验证凭证时:
客户端: HELLO 3 AUTH default mypassword
服务器: -ERR 密码无效
(连接保留在 RESP2 模式)
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
作为一个整体。
*多个命令和管线化
客户端可以在同一连接中多次发出命令。支持管线化,因此多个命令可以作为单个客户机写入操作发送,而无需等待每个回复。客户端可以跳过读取回复并继续一个接一个地发送命令。所有回复都可以在稍后读取。
更多信息,请参阅 Pipelining。
*内联命令
有时您可能需要向 Redis 服务器发送命令,但只有 telnet
可用。虽然 Redis 协议易于实现,但不适合交互式会话,且 redis-cli
可能无法使用。出于此原因,Redis 还接受命令的 内联命令 格式。
以下示例展示了 telnet 会话中的服务器/客户端交换(服务器消息以 S:
开头,客户端消息以 C:
开头):
C: PING
S: +PONG
以下是另一个示例,其中服务器返回一个整数:
C: EXISTS somekey
S: :0
基本上,要发出内联命令,您在 telnet 会话中写入空间分隔的参数。由于没有命令从 *
开头(作为 RESP 数组的标识字节),Redis 检测此条件并以内联方式解析您的命令。
*高性能的 Redis 协议解析器
尽管 Redis 协议易于阅读和实现,但其实现也可以达到类似二进制协议的性能。 RESP 使用前缀长度传输块数据,因此不需要在有效负载中扫描特殊字符(例如解析 JSON 时)。出于同样的原因,不需要对有效负载进行引号和转义。
要读取聚合类型(例如块字符串或数组)的长度,可以在不进一步处理的情况下逐字符处理。例如:
#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++;
}
/* 现在 p 变量指向 '\r',并且 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
- 在低级语言(如 C)中,处理RESP的解析可能会很耗时,需要精细的内存管理和错误处理。
- 高级语言(如 Python)可以利用其内置的字符串处理和异常机制来简化实现。
- 确保实现的健壮性,特别在处理大块数据和嵌套结构时,以避免潜在的缓冲区溢出或性能问题。
- 充分测试您的实现,以确保在各种边界条件下都能正确工作,例如空字符串、大整数和嵌套的聚合类型。
通过遵循这些提示,可以显著提高客户端的质量和可靠性。