learn.ochresign
译文 · 资料译文

Redis 序列化协议(RESP)规范

Redis 官方 RESP 规范的中文节选译文:RESP2 和 RESP3 的全部类型、握手、发送命令、内联命令和高性能解析。阶段 2 写解析器时对照着看。

译文说明原文:https://redis.io/docs/latest/develop/reference/protocol-spec/,作者 Redis Ltd.。原文按 CC BY-NC-SA 4.0 授权,本译文为节选翻译,有删改和补充说明,按相同授权提供,仅供非商业学习使用。

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