Redis协议规范

Redis协议规范

redis的客户端和redis服务器端的通信协议使用RESP(Redis Serialization Protocol,即Redis序列化协议)

RESP是以下几点的一个折衷方案:

  • 实现简单
  • 解析快速
  • 易于理解

RESP不仅可以序列化像整数、字符串、数组这样的类型,同时也可以针对错误类型。Redis客户端发送一个字符串数组格式的命令,Redis服务器回应命令相对应的数据类型

RESP是二进制安全的,同时不需要从一个进程到另一个进程的去处理数据块,因为传输的数据块是使用预定义长度

注意:这里所说的协议仅仅适用于客户端和服务器端的通信。Redis集群在两个节点之间交换数据使用不同的二进制协议

网络层

客户端通过TCP连接服务器端

虽然RESP在技术上并不是只针对TCP,但是通过这个协议的上下文来讲,这个协议只适用于TCP连接或者类似的面向流的连接(比如Unix sockets)

请求—响应模型

Redis接受由不同参数组成的命令,一旦一个命令被接受,这个命令就会被处理,并将处理结果返回给客户端

这可能是一个非常简单的模型,然而有两种意外:

  • Redis支持流水线技术,因此客户端可能一次发送多个命令,然后等待回复
  • 当一个Redis客户端订阅了一个Pub/Sub频道,这个协议改变了语义,成为推送协议。此时客户端不再需要发送指令,因为当服务器端接收到客户端所订阅的那个频道的新信息,服务器端将会自动的向客户端发送消息

RESP协议描述

RESP协议在Redis1.2中被引入,在Redis2.0中成为Redis服务器通讯的标准方式,因此你应该在你的Redis客户端中实现该协议

RESP实际上是一个序列化协议,支持简单字符串,大块字符串,错误,整数和数组类型

RESP在redis中作为请求响应协议使用:

  • 客户端发送给服务器端的命令是大块字符串类型的数组
  • 服务器端根据具体的命令回复RESP类型中的一种

在RESP中,数据的类型取决于第一个字符:

  • 简单字符串的第一个字符是+
  • 错误的第一个字符是-
  • 整数的第一个字符是:
  • 大块字符串的第一个字符是$
  • 数组的第一个字符是*

在RESP中,协议的不同部分总是以\r\n分割

RESP简单字符串

简单字符串是由一个+和不包含\r\n的字符串组成,以CRLF(\r\n)结尾

简单字符串是非二进制安全,在传输过程中开销较小。比方说许多Redis命令仅仅需要回复“OK”表示命令执行成功,使用RESP简单字符串编码只需要5比特:

+OK\r\n

于此相反,RESP大块字符串类型是二进制安全的

RESP错误类型

错误类型与简单字符串类型类似,但是第一个字符是-。错误类型是针对客户端的异常操作,其中包含错误提示信息。

基本格式为:

-Error message\r\n

只有在发送错误信息时才会发送错误回复,例如尝试对错误的数据类型进行操作,命令不存在等等

-WRONGTYPE Operation against a key holding the wrong kind of value
-ERR unknown command 'foobar'

-后的第一个单词一般表示为错误类型,但这仅仅是Redis使用时的约定,并不是RESP错误格式的一部分

比如说,ERR是一个一般性的错误,然而WRONGTYPE是一个更加具体的错误,指明了客户端尝试对错误的类型进行操作。这个被叫做错误前缀,用于在不依赖于可能会随着时间改变的确切错误消息的情况下,便于客户端理解服务器端返回的错误。这样的特性不应该认为它是至关重要的,因为它很少用的上

RESP整数

只是用CRLF结尾字符串来表示整型,用一个字节的:作为前缀。例如::0\r\n,或者:1000\r\n

整数也可以用于返回真和假

RESP大块字符串

大块字符串用于表示单个二进制安全的字符串,长度最大为512MB

大块字符串通过以下方式编码:

  • $开头,紧接着是字符串长度,以CRLF结尾
  • 实际的字符串
  • CRLF终止

字符串“foobar”编码如下

$6\r\nfoobar\r\n

空串表示为

$0\r\n\r\n

RESP大块字符串同样也可以用来表示单个不存在的值,表现为NULL

$-1\r\n

RESP数组

客户端使用RESP数组发送命令给服务器端,同样服务器端也有可能返回RESP数组格式的内容

RESP数组是由以下方式组成

  • *开头,紧接着是用十进制数表示的数组元素个数,以CRLF结尾
  • RESP类型的元素

空数组表示为

*0\r\n

带有两个RESP大块字符串"foo"和"bar"元素的数组编码为

*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\nfoobar\r\n

对于NULL值的数组(并非空数组),表示方法为

*-1\r\n

RESP还有可能出现数组的数组,比如说两个数组组成的数组

*2\r\n*3\r\n:1\r\n:2\r\n:3\r\n*2\r\n+Foo\r\n-Bar\r\n

在数组中的空元素

在数组中可能存在某一个元素为NULL

*3\r\n$3\r\nfoo\r\n$-1\r\n$3\r\nbar\r\n

第二个元素是一个NULL值,客户端可以理解为

["foo",nil,"bar"]

这并不是一个异常,这只是协议中的一种特殊情况

发送命令给服务器端

相信大家已经熟悉了RESP序列化格式,写一个Redis客户端也变得简单起来。

此时我使用linux中nc工具来进行实际的演示

Redis协议规范

客户端发送set mykey myvalue命令,服务器端返回简单字符串+OK

Redis协议规范

客户端发送get mykey命令,服务器端返回大块字符串$7\r\nmyvalue

Redis协议规范

批量命令以及流水线技术

一个客户端可以使用相同的连接来处理多条命令。支持流水线技术,客户端可以在一次写入操作中发送多条命令,不需要在发送下一个命令之前等待服务器对上一个命令的回复。所有的命令能够在最后被读到

Redis协议规范

内置命令

有些时候,你需要发送命令给Redis服务器,但手上只有telnet工具。虽然Redis协议易于实现,但在交互式会话中的使用并不理想,同时redis-cli并不总是可用的。基于这些原因,Redis接受另一种人们可读的命令,这种方式称为内嵌命令格式

Redis协议规范

基本上,在一个telnet会话里,你可以简单的书写空格分割的命令,由于命令没有像在统一的请求协议里的那样以*开头,所以Redis可以区分出这种情况,然后解析你的命令

Redis协议的高性能解析器

大块字符串以及批量的大块字符的处理可以使用对每一个字符进行单个操作,同时扫描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++;
    }

    /* 现在p指针指向了 '\r', 同时len是大块字符串的长度 */
    printf("%d\n", len);
    return 0;
}

在第一个CR字符确定后,可以在不做任何处理的情况下跳过LF字符,然后可以在不检查有效负载的情况下,单个读取操作就可以读取大块字符串,最终CRLF结尾符也可以在不做处理的情况下丢弃

与二进制协议相比,Redis协议更加易于在大部分的高级语言中实现,减少了客户端软件的bug数量