redis:通信协议
redis的作者认为数据库系统的瓶颈一般不在于网络流量,而在于数据库自身内部逻辑处理上,所以即使Redis使用了浪费流量的文本协议,依然可以取得极高的访问性能。redis将所有的数据都放在了内存中,用一个单线程对外提供服务,单个节点在跑满一个CPU核心的情况下可以达到10w/s的超过QPSRESPRESP是Redis序列化协议(REdis Serialization Portocol)的简写。它是
几乎所有的主流编程语言都有Redis的客户端,不考虑redis非常流行的原因,如果站在技术的角度看原因还有两个:
- 客户端与服务端之间的通信协议是在TCP协议之上构建的,服务端默认的端口号是6379
- 客户端和服务器发送的命令或者数据一律以
\r\n
(CRLF)结尾。 - redis制定了RESP实现了客户端与服务端的正常交互,这种协议简单,既能够被机器解析,又容易被人类识别。
RESP是Redis序列化协议(REdis Serialization Portocol)的简写。它是一种直观的文本协议,优势在于实现过程异常简单,解析性能极好。[官方文档]
RESP
Redis协议将传输的结构数据分为5种最小单元类型,单元结束时统一加上回车换行符号\r\n。
- 单行字符串以
+
符号开头
+hello world\r\n
- 多行字符串以
$
符号开头,后跟字符串长度
$11\r\nhello world\r\n
- 整数值以
:
开头,后跟整数的字符串形式
:1024\r\n // 整数1024
- 错误消息以
-
开头
-WRONGTYPE Operation against a key holding the wrong kind of value\r\n
- 数组以
*
开头,后跟数组的长度
*3\r\n:1\r\n:2\r\n:3\r\n //数组[1, 2, 3]
NULL:用多行字符串表示,不过长度要写成-1
$31\r\n
空串:用多行字符串表示,长度填0
$0\r\n\r\n
客户端—>服务器
客户端向服务器发送的指令只有一种格式,格式如下(CRLF代表"\r\n")。
- 新版统一请求协议在 Redis 1.2 版本中引入, 并最终在 Redis 2.0 版本成为 Redis 服务器通信的标准方式。
- 你的 Redis 客户端应该按照这个新版协议来进行实现。
- 在这个协议中, 所有发送至 Redis 服务器的参数都是二进制安全(binary safe)的。这个协议的一般格式如下
*<参数数量> CR LF
$<参数 1 的字节数量> CR LF
<参数 1 的数据> CR LF
...
$<参数 N 的字节数量> CR LF
<参数 N 的数据> CR LF
例如客户端发送一条set hello world命令给服务端,按照RESP 的标准,客户端需要将其封装为如下格式(每行用\r\n分隔):
*3
$3
SET
$5
hello
$5
world
- 参数数量为3个,因此第一行为:
*3
- 参数字节数分别是355,因此后面几行为:
$3
SET
$5
hello
$5
world
有一点要注意的是,上面只是格式化显示的结果,实际传输格式为如下代码:
*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n
稍后我们将会看到,这种格式除了用作命令请求协议之外,也用在命令的回复协议中:这种只有一个参数的回复格式叫做批量回复(Bulk Reply)
统一协议请求原本是用作回复协议中,用于将列表的多个项返回给客户端,这种回复格式被称为多条批量回复(Multi Bulk Reply)。
一个多条批量回复以*<argc>\r\n
为前缀, 后跟多条不同的批量回复, 其中 argc 为这些批量回复的数量。
服务器—>客户端
redis命令会返回多种不同协议的回复。通过检查服务器发回数据的第一个字节,可以确认这个回复是什么类型
·状态回复:在RESP中第一个字节为"+"。
·错误回复:在RESP中第一个字节为"-"。
·整数回复:在RESP中第一个字节为":"。
·字符串回复:在RESP中第一个字节为"$"。
·多条字符串回复:在RESP中第一个字节为"*"。
我们知道redis-cli只能看到最终的执行结果,那是因为redis-cli本身就是按照RESP进行结果解析的,所以看不到中间结果,redis-cli.c源码对命令结果的解析结构如下:
static sds cliFormatReplyTTY(redisReply *r, char *prefix) {
sds out = sdsempty();
switch (r->type) {
case REDIS_REPLY_ERROR:
// 处理错误回复
case REDIS_REPLY_STATUS:
// 处理状态回复
case REDIS_REPLY_INTEGER:
// 处理整数回复
case REDIS_REPLY_STRING:
// 处理字符串回复
case REDIS_REPLY_NIL:
// 处理空
case REDIS_REPLY_ARRAY:
// 处理多条字符串回复
return out;
}
例如执行set hello world,返回结果是OK,并不能看到加号:
为了看到Redis服务端返回的“真正”结果,可以使用nc命令、telnet命令、甚至写一个socket程序进行模拟
下面以nc命令进行演示,首先使用nc连接到Redis:
状态回复:set hello world的返回结果为+OK
状态回复
- 一个状态回复(或者单行回复,single line reply)是一段以
+
开始、\r\n
结尾的单行字符串。- 状态回复通常由那些不需要返回数据的命令返回,这种回复不是二进制安全的,他也不能包含新行
- 状态回复的额外开销非常少,只需要三个字节(开头的 “+” 和结尾的 CRLF)。
以下是一个状态回复的例子:
+OK
- 客户端库应该返回
+
号之后的所有内容。比如在上面的这个例子中, 客户端就应该返回字符串 “OK” 。
这里的OK就是单行响应
错误响应
- 错误回复和状态回复非常相似, 它们之间的唯一区别是, 错误回复的第一个字节是
-
, 而状态回复的第一个字节是+
。 - 错误回复只在某些地方出现问题时发送: 比如说, 当用户对不正确的数据类型执行命令, 或者执行一个不存在的命令, 等等。
- 一个客户端库应该在收到错误回复时产生一个异常。
以下是两个错误回复的例子:
-ERR unknown command 'foobar'
-WRONGTYPE Operation against a key holding the wrong kind of value
- 在 “-” 之后,直到遇到第一个空格或新行为止,这中间的内容表示所返回错误的类型。
- ERR 是一个通用错误,而 WRONGTYPE 则是一个更特定的错误。 一个客户端实现可以为不同类型的错误产生不同类型的异常, 或者提供一种通用的方式, 让调用者可以通过提供字符串形式的错误名来捕捉(trap)不同的错误。
不过这些特性用得并不多, 所以并不是特别重要, 一个受限的(limited)客户端可以通过简单地返回一个逻辑假(false)来表示一个通用的错误条件。
整数响应
- 整数回复就是一个以
:
开头, CRLF 结尾的字符串表示的整数。 - 比如说, “:0\r\n” 和 “:1000\r\n” 都是整数回复。
- 以下命令都返回整数回复:
- SETNX key value
- DEL key [key …]
- EXISTS key
- INCR key
- INCRBY key increment
- DECR key
- DECRBY key decrement
- DBSIZE
- LASTSAVE
- RENAMENX key newkey
- MOVE key db
- LLEN key
- SADD key member [member …]
- SREM key member [member …]
- SISMEMBER key member
- SCARD key 。
批量回复:多行字符串响应
服务器使用批量回复来返回二进制安全的字符串,字符串的最大长度是512MB。
客户端:GET mykey
服务器:foobar
服务端发送的内容中:
- 第一个字节是
$
符合 - 接下来跟着的是表示实际回复长度的数字值
- 之后跟着一个 CRLF
- 再后面跟着的是实际回复数据
- 最末尾是另一个 CRLF
对于前面的 GET key 命令,服务器实际发送的内容为:
"$6\r\nfoobar\r\n"
- 如果被请求的值不存在, 那么批量回复会将特殊值 -1 用作回复的长度值, 就像这样:
客户端:GET non-existing-key
服务器:$-1
-
这种回复称为空批量回复(NULL Bulk Reply)。
-
当请求对象不存在时,客户端应该返回空对象,而不是空字符串: 比如 Ruby 库应该返回 nil , 而 C 库应该返回 NULL (或者在回复对象中设置一个特殊标志), 诸如此类。
数组响应:多条批量回复
像LRANGE key start stop
这样的命令需要返回多个值, 这一目标可以通过多条批量回复来完成。
redis> RPUSH fp-language lisp
(integer) 1
redis> LRANGE fp-language 0 0
1) "lisp"
redis> RPUSH fp-language scheme
(integer) 2
redis> LRANGE fp-language 0 1
1) "lisp"
2) "scheme"
- 多条批量回复是由多个回复组成的数组,数组中的每个元素都可以是任意类型的回复,包括多条批量回复本身
- **多条批量回复的第一个字节为
*
,后跟一个字符串表示的整数值,这个值记录了多条批量回复所包含的回复数量,再后面是一个 CRLF **
客户端: LRANGE mylist 0 3
服务器: *4
服务器: $3
服务器: foo
服务器: $3
服务器: bar
服务器: $5
服务器: Hello
服务器: $5
服务器: World
在上面的示例中,服务器发送的所有字符串都由 CRLF 结尾。
正如你所见到的那样, 多条批量回复所使用的格式, 和客户端发送命令时使用的统一请求协议的格式一模一样。 它们之间的唯一区别是:
- 统一请求协议只发送批量回复。
- 而服务器应答命令时所发送的多条批量回复,则可以包含任意类型的回复。
以下例子展示了一个多条批量回复, 回复中包含四个整数值, 以及一个二进制安全字符串:
*5\r\n
:1\r\n
:2\r\n
:3\r\n
:4\r\n
$6\r\n
foobar\r\n
在回复的第一行, 服务器发送 *5\r\n , 表示这个多条批量回复包含 5 条回复, 再后面跟着的则是 5 条回复的正文。
- 多条批量回复也可以是空白的(empty), 就像这样:
客户端: LRANGE nokey 0 1
服务器: *0\r\n
- 无内容的多条批量回复(null multi bulk reply)也是存在的, 比如当 BLPOP key [key …] timeout 命令的阻塞时间超过最大时限时, 它就返回一个无内容的多条批量回复, 这个回复的计数值为 -1
客户端: BLPOP key 1
服务器: *-1\r\n
- 客户端库应该区别对待空白多条回复和无内容多条回复: 当 Redis 返回一个无内容多条回复时, 客户端库应该返回一个 null 对象, 而不是一个空数组。
多条批量回复中的空元素
多条批量回复中的元素可以将自身的长度设置为 -1 , 从而表示该元素不存在, 并且也不是一个空白字符串(empty string)。
当 SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern …]] [ASC | DESC] [ALPHA] [STORE destination]
命令使用 GET pattern 选项对一个不存在的键进行操作时, 就会发生多条批量回复中带有空白元素的情况。
以下例子展示了一个包含空元素的多重批量回复:
服务器: *3
服务器: $3
服务器: foo
服务器: $-1
服务器: $3
服务器: bar
其中, 回复中的第二个元素为空。
对于这个回复, 客户端库应该返回类似于这样的回复:
["foo", nil, "bar"]
例子:
响应为:
嵌套
即:
多命令和流水线
客户端可以通过流水线,在一次写入操作中发送多个命令:
- 在发送新命令之前,无需阅读前一个命令的回复
- 多个命令的回复会在最后一并返回
内联命令
当你需要和redis服务器进行沟通,但是又找不到redis-cli,而手上只有telnet
的时候,你可以通过redis特别为这种情况设计的内联命令格式来发送命令。
以下是一个客户端和服务器使用内联命令来进行交互的例子:
客户端: PING
服务器: +PONG
以下另一个返回整数值的内联命令的例子:
客户端: EXISTS somekey
服务器: :0
因为没有了统一请求协议中的*
项来声明参数的数量, 所以在 telnet 会话输入命令的时候, 必须使用空格来分割各个参数, 服务器在接收到数据之后, 会按空格对用户的输入进行分析(parse), 并获取其中的命令参数。
高性能redis协议分析器
redis协议非常利于阅读,定义也很简单,但这个协议的实现性能仍然可以和二进制协议一样快。
因为redis协议将数据的长度放在数据正文之前,所以程序无需像json那样,为了寻找某个特殊字符而扫描整个payload,也无需对发送服务器的payload进行转移。
程序可以在对协议文本中的各个字符进行处理的同时,查找 CR 字符,并计算出批量回复或者多条批量回复的长度,就像这样:
#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;
}
得到了批量回复或者多条批量回复的长度之后,程序只需要调用一次read
函数,就可以将回复的正文数据全部读入内存,而无需对这些数据进行任何处理
在回复最末尾的CR和LF不作处理,丢弃它们
小结
redis协议里有大量冗余的回车换行符,但是这并不影响它成为互联网技术领域非常受欢迎的一个文本协议。
有很多开源项目使用RESP作为它的通信协议。
在技术领域,性能并不总是一切,还有简单性、易理解性以及易实现性
更多推荐
所有评论(0)