Redis协议规范(译文)

代码星冰乐

专注成就未来

首页 归档 关于

Redis协议规范(译文)

Aug 8, 2018 | haifeiWu | Java | 阅读
文章目录
  1. 1. 网络层
  2. 2. 请求 - 响应模型
  3. 3. RESP 协议描述
  4. 4. RESP 单行字符串(简单字符串)
  5. 5. RESP 错误信息
  6. 6. RESP 整型数据
  7. 7. RESP 多行字符串
  8. 8. RESP 数组
  9. 9. 发送命令到 Redis 服务端
  10. 10. 多个命令和流水线操作
  11. 11. Inline Commands(内联命令)
  12. 12. 高效解析Redis协议
  13. 13. 小结
  14. 14. 参考链接

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2018/e687/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。


由于版权原因,请阅读原文 --> Redis协议规范(译文)

关注我们

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2018/e687/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2018/e687/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。

Redis客户端使用名为RESP(Redis序列化协议)的协议与Redis服务器进行通信。 虽然该协议是专为Redis设计的,但它可以用于其他CS软件项目的通讯协议。

RESP是以下几方面的考虑:

  • 易于实现
  • 快速解析
  • 可读性高

RESP可以序列化不同的数据类型,如整型,字符串,数组。 还有一种特定的错误类型。 请求将要执行的命令作为字符串数组从Redis客户端发送到Redis服务器。Redis使用特定数据类型的命令进行回复。

RESP是二进制安全的,不需要处理从一个进程传输到另一个进程的批量数据,因为它使用前缀长度来传输批量数据。

注意:此处概述的协议仅用于客户端 - 服务器通信。 Redis Cluster使用不同的二进制协议,以便在节点之间交换消息。

网络层

客户端连接到Redis服务器,是创建TCP连接到端口6379。
虽然RESP在技术上是非TCP特定的,但在Redis的上下文中,协议仅用于TCP连接(或类似的面向流的连接,如Unix套接字)。

请求 - 响应模型

Redis接受由不同参数组成的命令。 收到命令后,将对其进行处理并将回复发送回客户端。
这是最简单的模型,但有两个例外:

  • Redis支持流水线操作(本文档稍后介绍)。 因此,客户端可以一次发送多个命令,并等待稍后的回复。
  • 当Redis客户端处于 Pub/Sub 时,协议会更改语义并成为推送协议,即客户端不再需要发送命令,因为服务器会在它们接收到命令时发自动向客户端发送新消息。

排除上述两个例外,Redis协议是一个简单的请求 - 响应协议。

RESP 协议描述

RESP协议在Redis 1.2中引入,但它成为与Redis 2.0中的Redis服务器通信的标准方式。 这是每一个Redis客户端中应该实现的协议。

RESP实际上是一个支持以下数据类型的序列化协议:单行字符串,错误信息,整型,多行字符串和数组。
RESP在Redis中用作请求 - 响应协议的方式如下:

  • 客户端将命令作为字符串数组发送到Redis服务器。
  • 服务器根据命令实现回复一种RESP类型数据。

在 RESP 中, 一些数据的类型通过它的第一个字节进行判断:

  • 单行回复:回复的第一个字节是 “+”

  • 错误信息:回复的第一个字节是 “-“

  • 整形数字:回复的第一个字节是 “:”

  • 多行字符串:回复的第一个字节是 “$”

  • 数组:回复的第一个字节是 “*”

此外,RESP能够使用稍后指定的Bulk Strings或Array的特殊变体来表示Null值。
在RESP中,协议的不同部分始终以“\ r \ n”(CRLF)结束。

RESP 单行字符串(简单字符串)

简单字符串按以下方式编码:加号字符,后跟不能包含CR或LF字符的字符串(不允许换行),由CRLF终止(即“\ r \ n”)。

Simple Strings用于以最小的开销传输非二进制安全字符串。 例如,许多Redis命令成功回复时只有“OK”,因为RESP 单行字符串使用以下5个字节进行编码:

1
"+OK\r\n"

为了发送二进制安全字符串,使用RESP 多行字符串代替。

当Redis使用Simple String回复时,客户端库应该向调用者返回一个字符串,该字符串由“+”之后的第一个字符组成,直到字符串结尾,不包括最终的CRLF字节。

RESP 错误信息

RESP具有错误的特定数据类型。 实际上错误与RESP 单行字符串完全相同,但第一个字符是减号’ - ‘字符而不是加号。

RESP中单行字符串和错误之间的真正区别在于客户端将错误视为异常,组成错误类型的字符串是错误消息本身。
基本格式如下:

1
"-Error message\r\n"

错误回复仅在发生错误时发送,例如,如果您尝试对错误的数据类型执行操作,或者命令不存在等等。 收到错误回复时,客户端应将异常抛出。

以下是错误回复的示例:

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

“ - ”之后的第一个单词,直到第一个空格或换行符,表示返回的错误类型。 这只是Redis使用的约定,不是RESP错误格式的一部分。

例如,ERR是一般错误,而WRONGTYPE是一个更具体的错误,意味着客户端尝试对错误的数据类型执行操作。 这称为错误前缀,是一种允许客户端理解服务器返回的错误类型的方法,而不依赖于给定的确切消息,这可能随时间而变化。

客户端实现可以针对不同的错误返回不同类型的异常,或者可以通过直接将错误名称作为字符串提供给调用者来提供捕获错误的通用方法。

但是,这样的功能不应该被认为是至关重要的,因为它很少有用,并且有限的客户端实现可能只返回通用的错误条件,例如false。

RESP 整型数据

此类型只是一个CRLF终止的字符串,表示一个以“:”字节为前缀的整数。 例如“:0 \ r \ n”或“:1000 \ r \ n”是整数回复。
许多Redis命令返回RESP 整型,如INCR,LLEN和LASTSAVE。

返回的整数没有特殊含义,它只是INCR的增量编号,LASTSAVE的UNIX时间等等。 但是,返回的整数应保证在有符号的64位整数范围内。

整数回复也被广泛使用,以便返回真或假。 例如,EXISTS或SISMEMBER之类的命令将返回1表示true,0表示false。

如果实际执行操作,其他命令(如SADD,SREM和SETNX)将返回1,否则返回0。

以下命令将回复整数回复:SETNX,DEL,EXISTS,INCR,INCRBY,DECR,DECRBY,DBSIZE,LASTSAVE,RENAMENX,MOVE,LLEN,SADD,SREM,SISMEMBER,SCARD。

RESP 多行字符串

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

多行字符串按以下方式编码:

  • 一个“$”字节后跟组成字符串的字节数(一个前缀长度),由CRLF终止。
  • 字符串数据。
  • 最终的CRLF。

所以字符串“foobar”的编码如下:

1
"$6\r\nfoobar\r\n"

当只是一个空字符串时:

1
"$0\r\n\r\n"

RESP 多行字符串也可用于使用用于表示Null值的特殊格式来表示值的不存在。 在这种特殊格式中,长度为-1,并且没有数据,因此Null表示为:

1
"$-1\r\n"

当服务器使用Null 多行字符串回复时,客户端库API不应返回空字符串,而应返回nil对象。 例如,Ruby库应返回’nil’,而C库应返回NULL(或在reply对象中设置特殊标志),依此类推。

RESP 数组

客户端使用RESP 数组将命令发送到Redis服务器。 类似地,某些Redis命令将元素集合返回给客户端使用RESP 数组是回复类型。 一个例子是LRANGE命令,它返回列表的元素。

RESP数组使用以下格式发送:

  • *字符作为第一个字节,后跟数组中的元素数作为十进制数,后跟CRLF。
  • 数组的每个元素的附加RESP类型。

所以空数组就是以下内容:

1
"*0\r\n"

那么两个RESP批量字符串“foo”和“bar”的数组编码为:

1
"*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"

正如您在数组前面加上* CRLF部分之后所看到的那样,组成数组的其他数据类型将一个接一个地连接起来。 例如,三个整数的数组编码如下:

1
"*3\r\n:1\r\n:2\r\n:3\r\n"

数组可以包含混合类型,元素不必具有相同的类型。 例如,四个整数和批量字符串的列表可以编码如下:

1
2
3
4
5
6
7
*5\r\n
:1\r\n
:2\r\n
:3\r\n
:4\r\n
$6\r\n
foobar\r\n

服务器发送的第一行是* 5 \ r \ n,以指定将跟随五个回复。 然后发送构成多重回复项目的每个回复。

Null 数组的概念也存在,并且是指定Null值的替代方法(通常使用Null 多行字符串,但由于历史原因,我们有两种格式)。

例如,当BLPOP命令超时时,它返回一个计数为-1的Null数组,如下例所示:

1
"*-1\r\n"

当Redis使用Null数组回复时,客户端库API应返回空对象而不是空数组。 这是区分空列表和不同条件(例如BLPOP命令的超时条件)所必需的。

RESP中可以使用数组中嵌套数组。 例如,两个数组的数组编码如下:

1
2
3
4
5
6
7
8
*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。 客户端库应返回如下内容:

1
["foo",nil,"bar"]

注意,这不是前面部分中所述的例外,而只是进一步指定协议的示例。

发送命令到 Redis 服务端

既然熟悉RESP序列化格式,那么编写Redis客户端库的实现将很容易。 我们可以进一步讲述客户端和服务器之间的交互如何工作:

  • 客户端向Redis服务器发送仅由Bulk Strings组成的RESP阵列。
  • Redis服务器回复发送任何有效RESP数据类型作为回复的客户端。

因此,例如,典型的交互可以是以下所示。

客户端发送命令LLEN mylist以获取存储在密钥mylist中的列表长度,服务器回复一个Integer回复,如下例所示(C:是客户端,S:服务器)。

1
2
3
4
5
6
C: *2\r\n
C: $4\r\n
C: LLEN\r\n
C: $6\r\n
C: mylist\r\n
S: :48293\r\n

通常我们将协议的不同部分与换行符分开以简化,但实际的交互是客户端发送* 2 \ r \ n $ 4 \ r \ nLLEN \ r \ n $ 6 \ r \ nmylist \ r \ n整体。

多个命令和流水线操作

客户端可以使用同一个连接来发出多个命令。 支持流水线操作,因此客户端可以通过单个写入操作发送多个命令,而无需在发出下一个命令之前读取上一个命令的服务器回复,所有的回复都可以在最后阅读。

有关更多信息,请查看我们关于 Pipelining 的页面。

(译注: 对于基于像TCP这样的流式协议,Pipeling 实际上是一种协议的实现技术,站在服务端的角度就算它一次收到了多个命令,它也不知道客户端是一次发送了多个命令还是分了多次发送,但当服务器端一次收到多个命令时确实可以做一些优化处理,比如 优化 RTT, 多个命令的返回调用一次write系统调用从而减少系统调用的次数,提高吞吐量。)

Inline Commands(内联命令)

有时您只能通过 telnet 向 Redis 服务器发送命令,来测试可用性。 虽然Redis协议易于实现,但在交互式会话中使用并不理想,并且redis-cli可能并不总是可用。 出于这个原因,Redis 设计了一种特殊的接受命令的方式,并称为内联命令格式。

以下是使用内联命令进行服务器/客户端交互的示例(服务器聊天以S:开头,客户端与C聊天:)

1
2
C: PING
S: +PONG

以下是返回整数的内联命令的另一个示例:

1
2
C: EXISTS somekey
S: :0

基本上在telnet会话中你可以简单的写空格分割的参数。由于在协议请求中没有命令以 * 开头,Redis可以检测这种情况并处理命令。

高效解析Redis协议

尽管 Redis 协议非常易读且易于实现,但它却可以拥有二进制协议高效性能。

RESP 使用前缀长度来传输批量数据,因此永远不需要扫描有效负载以查找特殊字符,例如使用JSON,也不需要引用需要发送到服务器的有效负载。

可以使用对每个字符执行单个操作的代码处理批量和多个批量长度,同时扫描CR字符,如下面的C代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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;
}

在识别出第一个CR之后,可以将其与下面的LF一起跳过而不进行任何处理。 然后,可以使用不以任何方式检查有效负载的单个读取操作来读取批量数据。 最后,丢弃剩余的CR和LF字符而不进行任何处理。

与二进制协议比较性能时,Redis协议在大部分的高级语言实现起来足够简单,减少了客户端软件的bug数量。

(译注:

  1. 协议中的CR和LF相当于分割符,命令间存在多个CRLF不应影响后续解析,应为多个CRLF应被忽略掉。例如:
  2. 长度前缀是高效解析协议的关键。字段长度信息并不是二进制协议的专利,文本协议也可以有。
    )

小结

这是楼主第一次尝试翻译一篇技术文档,相对来说技术文档的英文阅读起来还是比较舒服的,相信有了第一次尝试,之后肯定会越来越顺利。由于楼主水平有限,文章中难免有纰漏,期望小伙伴的指出,感谢……。

参考链接

  • Redis Protocol specification
  • Pipelining

关注我们

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2018/e687/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。

分享
Redis译文
Netty源码中对Redis协议的实现线上 Elasticsearch 集群健康值 red 状态问题排查与解决
微信关注我们
分类
  • Android8
  • Go4
  • Java59
  • Kafka,Java1
  • Kotlin2
  • Linux1
  • MapReduce1
  • Python2
  • Raft1
  • Redis1
  • ThreadPoolExecutor1
  • go1
  • 工具1
  • 总结8
  • 旅游日记1
标签
Nginx ChanghuiN haifeiWu Android Java 设计模式 hexo Kotlin 算法 MySQL 源码解析 Python Redis golang web Kafka 配置中心 总结 性能优化 旅游日记 Shell Go 问题排查 译文 Docker Spring Boot 工具 学习笔记 WebFlux 性能测试 go 散列表 源码 netty Raft
最近文章
  • Kafka的日志复制机制
  • 从20到21
  • go 并发编程
  • 【译】了解Linux CPU负载-您何时应该担心?
  • Zookeeper 与分布式锁
  • 基于Redis的分布式锁到底安全吗?
  • 【译】Raft 学生指南
  • ThreadPoolExecutor 的简单梳理
  • MapReduce 的简单实现
  • 使用 Map 实现策略模式
福利专区
    免费SSL证书
      阿里云红包
        腾讯云专属福利
        Copyright © 2021 代码星冰乐. Powered by ChanghuiN. 版权所有 晋ICP备15001365号
        特别感谢: 云服务器服务商 、 CDN 服务商