Redis命令行通讯协议

Redis客户端通过RESP(REdis Serialization Protocol )协议与服务端通讯。Redis集群是则是通过另一协议来处理节点间的通讯。

RESP有以下特点:

  • 简单且容易实现
  • 能够快速解析
  • 可读的

RESP 能够序列化各种类型,包括整形,字符串,数组,错误类型. 客户端连接服务端的数据是以字符串数组的形式发送给服务端,相当于命令的参数形式。Redis 响应命令执行结果数据.

RESP 是二进制安全的,不需要从一个进程传送大量的数据,因为使用了前缀长度(prefixed-length)传送数据。

二进制安全

二进制化的字符串,不关心具体格式.只会严格的按照二进制的数据存取。不会妄图已某种特殊格式解析数据。也就是二进制安全的字符串是只有一种正确解。

网络层

Redis使用TCP进行客户端的连接,但是技术上不完全依赖TCP,更像是 Unix sockets

Unix sockets

Unix Domain Socket 是在Socket架构上发展起来的用于同一台主机的进程间通讯(IPC)。它不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序列号应答等。只是将应用层数据从一个进程拷贝到另一个进程。Unix Domain Socket有SOKCET_DGRAM和SOCKET_STREAM两种模式,类似于UDP和TCP,但是面向消息的Unix socket也是可靠的,消息既不会丢失也不会顺序错乱。比如的mysql socket文件:/tmp/mysql.sock, 直接操作该文件就可以与mysql通讯。

请求响应模型

Redis使用的是最简单的协议,接收一个命令参数时,会立即执行该命令并返回执行结果数据。

除以下情况,Redis都是通过这种简单协议通讯:

  • Redis 支持管道操作。导致一次性会有很多的命令发送后需要等待返回
  • Redis订阅了一个Pub/Sub 频道,redis将更改协议语义,此时转变成了push协议。客服端不在发送指令,而是由服务端发送数据到客户端。

RESP 协议描述

RESP实际上就是一个序列化协议。支持简单字符串、错误、整形,批量字符串和数组类型。

Redis 的请求响应协议方式有以下两种:

  • 所有客户端发送到服务端的命令都是一个字符串数组.
  • Redis根据指令的执行结果,回复一个相应的数据类型

RESP 通过第一个个字节来标记数据类型:

  • + 标记字符串
  • - 标记错误类型
  • : 标记整形
  • $ 标记批量字符串
  • * 标记数据类型

RESP 总是以\r\n (CRLF)为结束符。

简单字符串

简单字符串,及非二进制安全的字符串。使用+ 开始标记:

1
"+OK\r\n"

如果要发送二进制安全的字符串,则需要通过字符串数组方式。

错误类型

与简单字符串类似,不过是以-开头。如

1
"-Error message\r\n"

只有在发生错误时才会返回错误。

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

ERRWRONGTYPE 是错误类型前缀,以便区分不同的错误类型,ERR是基本的错误类型。

整形

:开头,例如:

1
":0\r\n"
1
":1000\r\n"

多用与INCR,LLEN,LASTSAVE等命令。整形的1,0可以用来表示真假。也可以用来表示指令是否成功执行

字符串数组(Bulk Strings)

用来传送二进制安全的字符串,高达512M长度的发送。

通过以下方式进行编码:

  • $开头接收一个长度的数字,拼接上CFLR\r\n
  • 再次连接实际的字符串
  • 以CFLR结束
    1
    "$6\r\nfoobar\r\n"

特别的空表示方式如下:

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

Null表示方式如下:

1
"$-1\r\n"

以上两种特别情况下,不同的API库不会返回一个可空的字符串,不同语言的封装包,中例如Ruby返回nil对象,C语言放回Null。

数组

LRANGE 命令返回的就是数组格式。

通过以下方式格式化:

  • *开头跟上数组的长度,以CRLF结尾。

    1
    "*0\r\n"
  • 数组元素是可选的

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

表示两个数组,由两个字符串$3\r\nfoo\r\n,$3\r\nbar\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

为了清晰可见这里做了换行处理。

特别的,Null数组表示方式:

1
"*-1\r\n"

数组的元素支持Null,如:

1
2
3
4
5
6
*3\r\n
$3\r\n
foo\r\n
$-1\r\n
$3\r\n
bar\r\n

为了清晰可见这里做了换行处理。不同语言包处理的结果不同,可能是一下结果:

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

原理实践

  • 客服端发送字符串数组到服务端
  • 服务端根据实际执行结果返回内容

如果我们要执行LLEN mylist
则客户端发送:

1
2
3
4
5
*2\r\n
$4\r\n
LLEN\r\n
$6\r\n
mylist\r\n

为了清晰可见这里做了换行处理,实际上发送的不能进行换行处理,实际发送内容是

1
*2\r\n$4\r\nLLEN\r\n$6\r\nmylist\r\n

服务端返回:

1
":3445\r\n"

多命令管道化

客户端可以在一个连接上提交多个命令,管道提供了一个可以一次提交多个命令的方法。

内联命令

如果我们手头上的没有redis-cli工具,只有telnet或者单纯的其他TCP连接工具,那么我么可以使用内联命令使用redis

1
telnet 127.0.0.1 6379

输入PING

1
2
3
4
5
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
PING
+PONG

例如可输入得到结果:

1
2
EXISTS somekey
:0