Apache Kvrocks 在 2023 年社区 Roadmap 规划[1] 时有用户提出希望支持 RESP3。在权衡整体工作量和用户收益之后,并没有着急在 2023 年推进实现。希望通过这篇文章澄清一下原因,以及介绍一下 RESP 2/3 之间的区别。
什么是 RESP ?
RESP(Redis serialization protocol) 是 Redis 1.2 引入的 Client/Server 网络序列化协议。在 Redis 2.0 演变为 RESP v2,直到 Redis 6 引入新版本的 RESP v3[2](统一简称为 RESP2 和 RESP3)。大部分用户接触 Redis 是在 2.0 以后(我没见到 1.x 的用户),所以谈到 Redis 协议指的是 RESP2/3。
相比于通用的序列化协议如 BSON/ MessagePack ProtoBuffer 等, RESP 的主要目标是:
易于实现 (Simple to implement)
快速解析 (Fast to parse)
内容可读 (Human readable)
RESP2
RESP 协议使用 \r\n
作为分隔符,支持的类型包含:
1. 整数
以 : 作为开头,格式为: :[<+|->]<value>\r\n
:1234\r\n
表示整数 1234:-567\r\n
表示整数 -567
2. 简单字符串
以 +
作为开头,格式为: +Simple String\r\n
例如,返回简单字符串 "OK",使用 RESP2 表示:
+OK\r\n
3. 复杂字符串(Bulk strings)
以 $
作为开头,格式为: $<length>\r\n<data>\r\n
例如,字符串 "hello" 在 RESP2 的表示:
$5\r\n
hello\r\n
其中,$5\r\n
表示字符串 "hello" 长度为 5
4. 简单错误(Simple Error)
以 -
作为开头,格式为: -Error message\r\n
例如,-ERR unknown command 'asdf'\r\n
表示命令不存在的错误。格式类似简单字符串,无法携带二进制错误信息。
5. 数组
以 *
作为开头,格式为: *<number-of-elements>\r\n<element-1>...<element-n>
例如,数组 [1, "foo"]
在 RESP2 的表示:
*2\r\n
+1\r\n
$3\r\nfoo\r\n
*2\r\n
表示该数组元素个数为 2,紧接着是数组元素的内容,分别是整数 1 和字符串 "foo"
6. 空(null)
在 RESP2 里面 null 有两种表示: $-1\r\n
表示字符串的 null,*-1\r\n
表示数组的 null
RESP2 设计问题
首先,缺少准确的数据类型。例如,SET 和 Map 类型导致只能通过数组来表示,所以客户端解析时需要知道当前是什么命令才能将返回结果解析成正确的数据类型:
如果是 HGETALL 命令,则将响应结果解析为 Map 类型
如果是 SMEMBERS 命令,则将响应结果解析为 Set 类型
如果是 LRANGE 命令,则将响应结果解析为 Array 类型
假设可返回精确的类型,那么客户端在解析的时候则可不需要关心当前命令即可直接进行转换。类似的,在 RESP2 里浮点数类型使用字符串表示,而布尔类型整数 0|1 来表示。
其次,null 有多种形式 $-1\r\n
和 *-1\r\n
,同样也需要按照命令来对应解释。
最后,错误信息只能是简单文本,无法携带二进制内容。
RESP3
为了解决这些问题,RESP3 引入以下几个新类型:
1. 布尔类型
以 #
作为开头, 格式为: #<t|f>\r\n
,其中 t
表示 true, f
表示 false
例如,#t\r\n
则表示返回值为 true
2. 空(null)
以 _
开头,格式固定为: _\r\n
3. 浮点数
以 ,
作为开头,格式为: ,[<+|->]<integral>[.<fractional>][<E|e>[sign]<exponent>]\r\n
例如 ,+1.23\r\n
表示浮点数 1.23,,-4.5\r\n
表示浮点数 -4.5
而在 RESP2 则只能使用字符串 $4\r\n1.23\r\n
来表示浮点数 1.23
4. 大数类型(Big Numbers)
以 (
作为开头,格式为: ([+|-]<number>\r\n
例如,大数 3492890328409238509324850943850943825024385
在 RESP3 里面的表示:
(3492890328409238509324850943850943825024385\r\n
注意,如果客户端不支持 Big Number 类型则应该转换为字符串类型。
5. 复杂错误类型(Bulk errors)
以 !
作为开头,格式为: !<length>\r\n<error>\r\n
形式上和复杂字符串是一致,只是开头字符不一样。例如:
!21\r\n
SYNTAX invalid syntax\r\n
6. 字典(Maps)
以 %
作为开头,格式为: %<number-of-entries>\r\n<key-1><value-1>...<key-n><value-n>
例如, { "first": 1, "second": 2 }
用 RESP3 表示:
%2\r\n
+first\r\n
:1\r\n
+second\r\n
:2\r\n
7. 集合(Sets)
以 ~
作为开头,格式为: ~<number-of-elements>\r\n<element-1>...<element-n>
例如,集合 [3, 10, 12]
用 RESP3 表示:
~2\r\n
+3\r\n
+10\r\n
+12\r\n
8. 推送(Push)
以 >
作为开头,格式为: ><number-of-elements>\r\n<element-1>...<element-n>
这个命令的主要作用是 Redis Server 主动推送数据给客户端, Client-side Caching 实现需要依赖 Push 协议来将更新数据推送给你到客户端。
9. Verbatim strings
以 =
作为开头,格式为: =<length>\r\n<encoding>:<data>\r\n
其中 encoding 使用固定 3 个字符表示数据的编码方式,紧接着 :
用来分割编码和内容。比如是纯文本:
=15\r\n
txt:Some string\r\n
Redis 请求/响应
对于 Redis 服务来说,请求(request)统一都使用字符串数组(bulk strings array),而像整数 错误 / 简单字符串 null 这些只会在返回响应(response)里使用。
以 SET hello hulk
命令为例,变成 Redis 请求则是:
第一个元素是字符串,长度 3 第三个元素是字符串,长度 4
| |
v v
*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$4\r\nhulk\r\n
^ ^
| |
数组长度 3 第二个元素是字符串,长度 5
协议解析流程:
第一个字符遇到
*
则可知当前是一个数组,接着解析数组长度直到分隔符(\r\n
),这里数组长度是3
,接着读取数组元素;开始解析数组第一个元素,看到
$
则开始进入解析字符串长度直到分隔符, 读取到一个元素长度为3
, 接着读取到内容SET\r\n
,长度符合预期则继续解析下一个元素,否则抛错,以同样的方式继续解析数组后面的两个元素接着 Redis 开始处理请求,请求如果正确被处理则返回:
+OK\r\n
, 出错则是:-ERR {message}\r\n
。
上面也提到 Redis 的请求(Request) 统一使用字符串数组的格式,第一个参数是命令,其他为命令参数。所以,请求的数组元素一定是字符串,只有 Client 解析响应内容的时候才会有多种类型。
RESP2 和 RESP3 兼容?
一开始 antirez[3] (Redis 作者) 打算在 Redis 6 直接强制升级到 RESP3,也就是不打算兼容 RESP2 客户端。理由也在文章: Why RESP3 will be the only protocol supported by Redis 6[4] 里面阐述。
核心观点是:
留给用户的时间总共有 2-3 年,时间上是充分的 强制升级可以让客户端主动要做好兼容准备,否则即使到 Redis 7 仍然会是现在的状态 RESP3 向前兼容 RESP2,客户端升级后仍然可以访问之前的 Redis 服务
这个想法也受到不少质疑,网友 Marc Gravell 认为这个决定过于激进。除了客户端之外,应用以及 Lua 脚本全部都要升级,这对于用户伤害过大。
最后 Marc Gravell 提出可以通过协商的方式来确定协议版本,antirez 也认为想法不错,也就有了后面的 HELLO[5] 命令:
HELLO [protover [AUTH username password] [SETNAME clientname]]
Redis 请求在建立连接时默认使用 RESP2 模式并发送 Hello 命令,第一个参数 protover
用来指定预期使用 2 还是 3 的协议格式。
例如,预期使用 RESP3,则发送 HELLO 3\r\n
,客户端最终通过返回里面的 proto
字段来判断 Server 是否支持 RESP2/3(Kvrocks 当前始终都返回 2)。当然,如果连 Hello 命令都不支持也默认是 RESP2 协议模式。
2020 年的时候,antirez 也在 twitter 上表示不会废弃 RESP2:

Redis 社区文档也提到未来废弃 RESP2 概率不大,但可能会要求新特性使用 RESP3 实现:
Future versions of Redis may change the default protocol version, but it is unlikely that RESP2 will become entirely deprecated. It is possible, however, that new features in upcoming versions will require the use of RESP3.
具体见: https://redis.io/docs/reference/protocol-spec/[6]
最后
以下仅仅是个人观点不代表 Apache Kvrocks 社区
从上面的协议可以看到 RESP3 和 RESP2 核心差异在于细化更多数据类型了,主要目的是让客户端实现不用逐个命令地翻译,对于用户使用上并没有本质区别。这也是为什么没有着急推进 RESP3 落地的主要原因。
从 antirez 和社区的态度来看,RESP2 仍然会存在挺长一段时间,不用担心社区突然废弃 RESP2 的问题。但毕竟同时维护两个版本还是有一些工作量(虽然不大),随着未来的版本逐步升级会让大部分用户自然切换到 RESP3,最终也可能会放弃对 RESP2 的兼容。对于 Kvrocks 来说,为了长期兼容性以及新特性(Client-side Caching)实现,应该会在未来(2024 年)支持 RESP3 协议。
参考资料
2023 年社区 Roadmap 规划: https://github.com/apache/kvrocks/discussions/1226
[2]RESP v3: https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md
[3]antirez: http://antirez.com/
[4]Why RESP3 will be the only protocol supported by Redis 6: http://antirez.com/news/125
[5]HELLO: https://redis.io/commands/hello/
[6]https://redis.io/docs/reference/protocol-spec/: https://redis.io/docs/reference/protocol-spec/




