Redis是怎样通讯的?
模型
Redis 协议模型就是简单的请求-响应模型,和平常的 Http 协议有点类似。客户端发送 Redis 命令,然后服务端处理命令并返回结果给客户端。Redis 官方说这可能是最简单的网络协议模型了。
有两种情况下不 不适用这个模型,一个是批量流水线命令,一个是发布/订阅功能。
协议描述
Redis 协议一般简单的分为 5 类数据结构,简单字符串、错误信息、数值、大字符串、数组。每种数据类型在第一个字节用不同的符号来区分:
- 简单字符串(Simple Strings):开头第一个符号为
+
,对应 HEX 值为:0x2b
- 错误信息(Errors):第一个字节符号为
-
,对应 HEX 值为:0x2d
- 数值(Integers):第一个字节符号为
:
,对应 HEX 值为:0x3a
- 大字符串(Bulk Strings):第一个字节符号为
$
,对应 HEX 值为:0x24
- 数组(Arrays):第一个字节符号为
*
,对应 HEX 值为:0x2a
这 5 种数据类型可以组合起来使用,每种数据类型通过 CRLF 结尾,就是平常的 \r\n
,对应的 HEX 值为:0x0d,0x0a
。一般我们判断一种数据类型是否结束时,只要判断是否有 \r
出现就可以了。Redis 客户端和服务端之间就是通过这些规则来进行通信的。
简单字符串
一般简单字符串用于返回 Redis 服务端的系统响应,如果要响应用户存储的数据时,一般会用大字符串(Bulk Strings)的数据类型来返回。
比如说客户端发送 set 命令新增一个 Key 来存储字符串,此时客户端就会返回 +OK
。这种方式返回的数据不能有空格和换行,因为空格和换行表示该类型的数据结尾。
redis:0>set name 灰灰
"OK"
# Redis 服务端响应数据
0000 2b 4f 4b 0d 0a +OK··
错误信息
当我们执行的命令发生错误时,Redis 服务端就会返回错误信息
redis:0>incr name
"ERR value is not an integer or out of range"
# Redis 服务端响应数据
0000 2d 45 52 52 20 76 61 6c 75 65 20 69 73 20 6e 6f -ERR val ue is no
0010 74 20 61 6e 20 69 6e 74 65 67 65 72 20 6f 72 20 t an int eger or
0020 6f 75 74 20 6f 66 20 72 61 6e 67 65 0d 0a out of r ange··
数值
返回数值的其中一种情况就是执行 exists
命令来判断某个 Key 存不存在,返回 1 表示存在,返回 0 表示不存在
redis:0>exists name
"1"
# Redis 服务端响应数据
0000 3a 31 0d 0a :1··
---------------------------------------------------------------
redis:0>exists hui
"0"
# Redis 服务端响应数据
0000 3a 30 0d 0a :0..
大字符串
大字符串返回值有两部分组成,一部分是表示字符串长度的数据,一部分是字符串本身数据。它由 $
符号开头,后面跟着的是表示字符串长度的数据,该数值直接用字符串的形式表示,也就说读取该字节数据的时候,要用读取字符串数据的方式来读取。读取完后再转换为数值数据,然后再根据这个数值来读取相对应长度的字节数据。这样数据中就可以包含空格和换行了,因为是根据开头的长度数值来读取相对应的字节数据的,而不是通过判断 \r\n
符号来读取。
比如客户端获取前面设置的 Key 为 name 的数据:
redis:0>get name
"灰灰"
# Redis 服务端响应数据
0000 24 36 0d 0a e7 81 b0 e7 81 b0 0d 0a $6..........
其中 e7 81 b0 e7 81 b0
就是 灰灰
字符的字节数据
数组
当服务端返回数组数据时,它由 *
符号开头,后面紧跟着的是这个数值的长度,和大字符串的字节长度一样,该长度也是以字符串的形式返回。数组中的每个元素再通过相对应的数据类型来表示。
*2
+value1
+value2
# or
*3
:22
:52
:99
# 当然也可以表示嵌套数组
*2
*1
:123
*2
:433
:92
比如客户端设置一个 list 数据,然后获取它
redis:0>lpush mylist value1 value2
2
# 获取 list
redis:0>lrange mylist 0 1
["value2","value1"]
0000 2a 32 0d 0a 24 36 0d 0a 76 61 6c 75 65 32 0d 0a *2..$6..value2..
0010 24 36 0d 0a 76 61 6c 75 65 31 0d 0a $6..value1..
发送命令
当客户端发送命令给服务端时,客户端一般会把命令组装成上面的数组加大字符串的数据格式再发送给服务端。比如上面的我们发送一个简单的新增一个 Key 命令:
redis:0>set name 灰灰
# 客户端发送给服务端端数据
0000 2a 33 0d 0a 24 33 0d 0a 73 65 74 0d 0a 24 34 0d *3..$3..set..$4.
0010 0a 6e 61 6d 65 0d 0a 24 36 0d 0a e7 81 b0 e7 81 .name..$6.......
0020 b0 0d 0a ...
Redis 数据结构
Redis 一般常用的有 5 种数据类型,下面看看 5 种数据类型对应的客户端和服务端之间的数据是怎么交互的。
字符串 String
redis:0>set name greycode
0000 2a 33 0d 0a 24 33 0d 0a 73 65 74 0d 0a 24 34 0d *3..$3..set..$4.
0010 0a 6e 61 6d 65 0d 0a 24 38 0d 0a 67 72 65 79 63 .name..$8..greyc
0020 6f 64 65 0d 0a ode..
# 响应
0000 2b 4f 4b 0d 0a +OK..
-------------------------------------------------------------------------
redis:0>get name
0000 2a 32 0d 0a 24 33 0d 0a 67 65 74 0d 0a 24 34 0d *2..$3..get..$4.
0010 0a 6e 61 6d 65 0d 0a .name..
# 响应
0000 24 38 0d 0a 67 72 65 79 63 6f 64 65 0d 0a $8..greycode..
哈希表 Hash
redis:0>hset myHash name huihui
0000 2a 34 0d 0a 24 34 0d 0a 68 73 65 74 0d 0a 24 36 *4..$4..hset..$6
0010 0d 0a 6d 79 48 61 73 68 0d 0a 24 34 0d 0a 6e 61 ..myHash..$4..na
0020 6d 65 0d 0a 24 36 0d 0a 68 75 69 68 75 69 0d 0a me..$6..huihui..
# 响应
0000 3a 31 0d 0a :1..
-------------------------------------------------------------------------
redis:0>hget myHash name
0000 2a 33 0d 0a 24 34 0d 0a 68 67 65 74 0d 0a 24 36 *3..$4..hget..$6
0010 0d 0a 6d 79 48 61 73 68 0d 0a 24 34 0d 0a 6e 61 ..myHash..$4..na
0020 6d 65 0d 0a me..
# 响应
0000 24 36 0d 0a 68 75 69 68 75 69 0d 0a $6..huihui..
-------------------------------------------------------------------------
redis:0>hgetall myHash
0000 2a 32 0d 0a 24 37 0d 0a 68 67 65 74 61 6c 6c 0d *2..$7..hgetall.
0010 0a 24 36 0d 0a 6d 79 48 61 73 68 0d 0a .$6..myHash..
# 响应
0000 2a 32 0d 0a 24 34 0d 0a 6e 61 6d 65 0d 0a 24 36 *2..$4..name..$6
0010 0d 0a 68 75 69 68 75 69 0d 0a ..huihui..
列表 List
redis:0>lpush lists huihui greycode
0000 2a 34 0d 0a 24 35 0d 0a 6c 70 75 73 68 0d 0a 24 *4..$5..lpush..$
0010 35 0d 0a 6c 69 73 74 73 0d 0a 24 36 0d 0a 68 75 5..lists..$6..hu
0020 69 68 75 69 0d 0a 24 38 0d 0a 67 72 65 79 63 6f ihui..$8..greyco
0030 64 65 0d 0a de..
# 响应
0000 3a 32 0d 0a :2..
-------------------------------------------------------------------------
redis:0>lrange lists 0 1
0000 2a 34 0d 0a 24 36 0d 0a 6c 72 61 6e 67 65 0d 0a *4..$6..lrange..
0010 24 35 0d 0a 6c 69 73 74 73 0d 0a 24 31 0d 0a 30 $5..lists..$1..0
0020 0d 0a 24 31 0d 0a 31 0d 0a ..$1..1..
# 响应
0000 2a 32 0d 0a 24 38 0d 0a 67 72 65 79 63 6f 64 65 *2..$8..greycode
0010 0d 0a 24 36 0d 0a 68 75 69 68 75 69 0d 0a ..$6..huihui..
集合 Set
redis:0>sadd myset hello hi
0000 2a 34 0d 0a 24 34 0d 0a 73 61 64 64 0d 0a 24 35 *4..$4..sadd..$5
0010 0d 0a 6d 79 73 65 74 0d 0a 24 35 0d 0a 68 65 6c ..myset..$5..hel
0020 6c 6f 0d 0a 24 32 0d 0a 68 69 0d 0a lo..$2..hi..
# 响应
0000 3a 32 0d 0a :2..
-------------------------------------------------------------------------
redis:0>smembers myset
0000 2a 32 0d 0a 24 38 0d 0a 73 6d 65 6d 62 65 72 73 *2..$8..smembers
0010 0d 0a 24 35 0d 0a 6d 79 73 65 74 0d 0a ..$5..myset..
#响应
0000 2a 32 0d 0a 24 35 0d 0a 68 65 6c 6c 6f 0d 0a 24 *2..$5..hello..$
0010 32 0d 0a 68 69 0d 0a 2..hi..
有序集合 ZSet
redis:0>zadd myZset 1 hello 2 world
0000 2a 36 0d 0a 24 34 0d 0a 7a 61 64 64 0d 0a 24 36 *6..$4..zadd..$6
0010 0d 0a 6d 79 5a 73 65 74 0d 0a 24 31 0d 0a 31 0d ..myZset..$1..1.
0020 0a 24 35 0d 0a 68 65 6c 6c 6f 0d 0a 24 31 0d 0a .$5..hello..$1..
0030 32 0d 0a 24 35 0d 0a 77 6f 72 6c 64 0d 0a 2..$5..world..
# 响应
0000 3a 32 0d 0a :2..
-------------------------------------------------------------------------
redis:0>zrange myset 0 -1
0000 2a 34 0d 0a 24 36 0d 0a 7a 72 61 6e 67 65 0d 0a *4..$6..zrange..
0010 24 36 0d 0a 6d 79 5a 73 65 74 0d 0a 24 31 0d 0a $6..myZset..$1..
0020 30 0d 0a 24 32 0d 0a 2d 31 0d 0a 0..$2..-1..
# 响应
0000 2a 32 0d 0a 24 35 0d 0a 68 65 6c 6c 6f 0d 0a 24 *2..$5..hello..$
0010 35 0d 0a 77 6f 72 6c 64 0d 0a 5..world..
Pipeline
Redis 执行 Pipeline 命令,可以一次用一个数据包发送多条命令,然后服务端在把所有命令的执行结果打包成一个数据包返回给客户端,这样在执行大量命令的时候可以减少系统开销。比如我们要执行 set num 998
和 incr num
这两条命令,那么我们就可以把这两条命令分别组装然后拼接在一起发送给服务端。
0000 2a 33 0d 0a 24 33 0d 0a 73 65 74 0d 0a 24 33 0d *3..$3..set..$3.
0010 0a 6e 75 6d 0d 0a 24 33 0d 0a 39 39 38 0d 0a 2a .num..$3..998..*
0020 32 0d 0a 24 34 0d 0a 69 6e 63 72 0d 0a 24 33 0d 2..$4..incr..$3.
0030 0a 6e 75 6d 0d 0a .num..
服务端接收到数据包后,处理每条命令,然后把结果打包返回。
0000 2b 4f 4b 0d 0a 3a 39 39 39 0d 0a +OK..:999..
其中, set num 998
命令的结果为:OK
incr num
的结果为:999
代码实现
了解了以上交互协议的规则后,我们就可以自己手写一个 Redis 客户端来与 Redis 服务器通讯了。
完整代码地址:https://gist.github.com/greycodee/4a102aa9ae689aea1874b1fe06190192