Redis 序列化协议(RESP)规范
Redis 官方 RESP 规范的中文节选译文:RESP2 和 RESP3 的全部类型、握手、发送命令、内联命令和高性能解析。阶段 2 写解析器时对照着看。
RESP 是 Redis 客户端和服务器之间通信用的协议。它的设计目标有三个:实现简单、解析快、人能直接读懂。客户端把命令编码成由 bulk string 组成的数组发给服务器,服务器可以用任意一种 RESP 类型回复。RESP 只用于客户端和服务器之间;Redis Cluster 节点之间使用另一套二进制协议。
RESP 的版本
- RESP2:从 Redis 2.0 开始成为标准协议。
- RESP3:大体上是 RESP2 的超集。Redis 6.0 开始以“可选、实验性”的方式支持它,但不支持流式字符串和流式聚合类型。
- 客户端可以用
HELLO命令握手,把连接升级到更高的协议版本。 - 从 Redis 7 开始,RESP2 和 RESP3 客户端都可以调用全部核心命令,但同一个命令在两种协议下的回复格式可能不同。
译注:做 mini-Redis 只需要实现 RESP2。redis-cli 和 redis-benchmark 默认都使用 RESP2。
RESP2 的类型
每个 RESP 值的第一个字节表示它的类型,每一段都以 \r\n(CRLF)结尾。
| 类型 | 首字节 | 格式 |
|---|---|---|
| 简单字符串(Simple string) | + |
+OK\r\n |
| 简单错误(Simple error) | - |
-ERR unknown command 'asdf'\r\n |
| 整数(Integer) | : |
:[<+\|->]<value>\r\n |
| 定长字符串(Bulk string) | $ |
$<长度>\r\n<数据>\r\n |
| 数组(Array) | * |
*<元素个数>\r\n<元素1>...<元素n> |
简单字符串
简单字符串里不能包含 CR 或 LF。需要传输二进制数据时,应该使用 bulk string。
简单错误
客户端收到简单错误时,应当把它当作异常抛出。错误信息开头的大写单词(比如 ERR、WRONGTYPE)是 Redis 的惯例,叫作错误前缀,它并不属于 RESP 类型本身的定义。
整数
整数是有符号的十进制 64 位整数。SISMEMBER、SADD 等命令用 1 和 0 表示布尔值。
定长字符串
bulk string 是二进制安全的。Redis 默认把它的长度上限设为 512 MB(配置项 proto-max-bulk-len)。
$5\r\nhello\r\n -> "hello"
$0\r\n\r\n -> 空字符串
空 bulk string($-1\r\n)表示值不存在,比如对不存在的 key 执行 GET。客户端应该返回 nil 对象,而不是空字符串。
数组
数组可以嵌套,也可以混合不同的类型:
*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n
*3\r\n:1\r\n:2\r\n:3\r\n
*0\r\n -> 空数组
空数组(*-1\r\n)用于表示“没有结果”,比如 BLPOP 超时。客户端应该返回 null 对象,而不是空数组,这样调用方才能区分这两种情况。
数组中的空元素用空 bulk string 表示。例如执行 SORT ... GET pattern 时某个 key 不存在:
*3\r\n$5\r\nhello\r\n$-1\r\n$5\r\nworld\r\n
→ ["hello", nil, "world"]
RESP2 里的“映射”
RESP2 没有映射(map)类型。映射用一个扁平数组表示,键和值交替排列:key1, value1, key2, value2, ...。HGETALL 的回复就是这样。
RESP3 新增的类型
RESP3 增加了下面这些类型。客户端应尽量把它们映射为编程语言自带的对应类型。
| 类型 | 首字节 | 格式 |
|---|---|---|
| 空值(Null) | _ |
_\r\n |
| 布尔(Boolean) | # |
#t\r\n 或 #f\r\n |
| 浮点数(Double) | , |
,1.23\r\n、,inf\r\n、,-inf\r\n、,nan\r\n |
| 大整数(Big number) | ( |
(3492890328409238509324850943850943825024385\r\n |
| 定长错误(Bulk error) | ! |
!21\r\nSYNTAX invalid syntax\r\n |
| 原样字符串(Verbatim string) | = |
=15\r\ntxt:Some string\r\n |
| 映射(Map) | % |
%<条目数>\r\n<键1><值1>... |
| 属性(Attribute) | \| |
格式同映射,但它是附加在下一个回复上的辅助数据 |
| 集合(Set) | ~ |
~<元素个数>\r\n<元素1>... |
| 推送(Push) | > |
格式同数组,用于传递带外数据 |
补充说明:
- 浮点数的小数部分可以省略,所以
:10\r\n和,10\r\n都是合法的。 - 原样字符串开头三个字节是编码提示(这里是
txt),redis-cli这类工具可以据此把内容原样显示出来。 - 属性应该和回复分开读取、单独保存。例如
MGET a b可能先返回一个带有key-popularity信息的|1 ...属性,然后才是数组[2039123, 9543892]。 - 推送可以出现在任意回复的前面或后面,也可以单独出现,但永远不会嵌套在其他类型里面。RESP3 下的 Pub/Sub 就使用推送类型。
Redis 本身不会发出流式字符串($?)或流式聚合类型(*?、~?、%?,以 . 结尾),不过 RESP3 规范允许模块使用它们。
客户端握手
新连接应该先发送 HELLO 命令。不发的话,连接默认处于 RESP2 模式。
Client: HELLO 4
Server: -NOPROTO sorry, this protocol version is not supported.
Client: HELLO 3
Server: -ERR unknown command 'HELLO' ← 服务器只支持 RESP2
Client: HELLO 3 AUTH default mypassword
Server: -ERR invalid password ← 连接仍处于 RESP2
HELLO 成功时返回一个映射。其中 server、version、proto 三个字段是必需的;Redis 还会返回 id、mode(standalone、sentinel 或 cluster)、role(master 或 replica)和 modules。
发送命令
客户端发送一个由 bulk string 组成的数组,服务器用任意合法的 RESP 类型回复:
C: *2\r\n$4\r\nLLEN\r\n$6\r\nmylist\r\n
S: :48293\r\n
管道(Pipelining)
客户端可以在一次写操作中连续发送多条命令,不等待每条命令的回复,最后再一次性读取所有回复。
译注:这就是阶段 3 用
redis-benchmark -P 16测出来吞吐量能翻好几倍的原因。服务器一次 read 读进多条命令,解析器要能循环处理完缓冲区里所有完整的命令。
内联命令
为了方便用 telnet 这类工具手动交互,Redis 也接受用空格分隔的“内联命令”。之所以可行,是因为没有任何命令以 * 开头,服务器看首字节就能区分两种格式。
C: PING
S: +PONG
C: EXISTS somekey
S: :0
高性能解析
bulk 数据带有长度前缀,所以解析器不需要扫描数据内容来寻找特殊字符,也不需要做转义。解析器逐位读取长度,直到遇到 CR,跳过 CRLF,然后一次性读出全部数据,再丢弃末尾的 CRLF。
#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;
}
给客户端作者的提示
想测试 RESP2 和 RESP3 下的各种回复,可以借助 Lua 的类型转换。例如下面这条命令会产生一个 RESP3 浮点数:
EVAL "return { double = tonumber(ARGV[1]) }" 0 1e0