深入学习redis

Posted 代码科学家

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入学习redis相关的知识,希望对你有一定的参考价值。

免责声明: 本人水平有限,难免有疏漏的地方。如果读者遇到文章中需要改进或者看不懂,甚至是觉得错误的地方,可以给我留言。

又给自己挖了一个(巨)坑。因为后端技术栈就那几个东西,总得去学习的,为了以后出去吹牛也好,为了提升自己也好,持续的深入学习是很有必要的。那为什么要选择redis?因为说实话,我现在的水平对redis的理解就停留于get和set,通过这个机会想好好给自己上一课。

介绍

Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库... 等等我干嘛要介绍这些东西,我给自己文章的定义就是硬核向的,所以一切基础的介绍,包括怎么安装,怎么使用,我都不会再去介绍,这些东西随手百度就能知道了,默认大家都已经是知道redis是什么并且都已经会基本的使用了。什么?你说你不会安装?点个关注,给我留言,我一对一手把手教你!

版本简要介绍

  • 3.0
    • Redis Cluster,推出了官方的集群方案
  • 4.0
    • 加入了模块系统,允许用户通过自己编写的代码来扩展和实现 Redis 本身并不具备的功能
  • 5.0
    • 新的Stream数据类型,有了Stream类型,redis就真的可以作为消息队列的中间件来使用了,之前版本如果使用发布订阅来实现消息队列的话,是有缺陷的。
  • 6.0
    • 多线程IO,众所周知redis是单线程的模型,在6.0之后官方支持多线程模型(不过默认是关闭的),为了能充分的利用CPU的资源。
    • 新的RESP3协议,在兼容RESP2上推出的新协议。
    • 客户端缓存

通信

redis和其他众多的技术一样(mysql,Zookeeper等),都是C/S的架构,即客户端/服务端。通常都有一个服务端进程(暂时不考虑集群),和N个客户端组成,而官方一般都推出服务端和客户端的命令行工具(或库),在redis中是redis-serverredis-cli分别对应服务端和客户端。 服务端一般启动后,会等待客户端的连接及连接后发送过来的命令,而且服务端和客户端通常都不会在同一台计算机上,就会涉及到网络通信,那么服务端和客户端是怎么通信的?这就会涉及另一个重要的概念就是协议。常见的协议TCP,HTTP等,这里我不会展开(因为我压根也不懂啊),而redis中的协议就是RESPREdis Serialization Protocol)。关于协议我之后在展开,这里可以把协议理解为由作者自己定义的一种约定,客户端和服务端都按照这样的约定去处理(解码或编码)发送和接收到的消息。

Hello World

redis中最常用的就是我之前提到的get/set命令,我们就从这两条简单的命令出发,去学习redis中客户端和服务端之间是怎么通信的,Let's go! 我在我的本地已经安装了redis,首先先用redis-server启动服务端

$ ./redis-server
4802:C 03 Jun 2020 23:45:40.889 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
4802:C 03 Jun 2020 23:45:40.889 # Redis version=6.0.4, bits=64, commit=00000000, modified=0, pid=4802, just started
4802:C 03 Jun 2020 23:45:40.889 # Warning: no config file specified, using the default config. In order to specify a config file use ./redis-server /path/to/redis.conf
4802:M 03 Jun 2020 23:45:40.890 * Increased maximum number of open files to 10032 (it was originally set to 256).
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 6.0.4 (00000000/0) 64 bit
.-`` .-```. ```/ _.,_ ''-._
( ' , .-` | `, ) Running in standalone mode
|`-._`-...-` __...-.``-._|'
` _.-'| Port: 6379
| `-._ `._ / _.-'
| PID: 4802
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'
_.-' | http://redis.io
`-._ `-._`-.__.-'
_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'
_.-' |
`-._ `-._`-.__.-'
_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'


4802:M 03 Jun 2020 23:45:40.894 # Server initialized
4802:M 03 Jun 2020 23:45:40.895 * Loading RDB produced by version 6.0.4
4802:M 03 Jun 2020 23:45:40.895 * RDB age 5402 seconds
4802:M 03 Jun 2020 23:45:40.895 * RDB memory usage when created 0.95 Mb
4802:M 03 Jun 2020 23:45:40.896 * DB loaded from disk: 0.001 seconds
4802:M 03 Jun 2020 23:45:40.896 * Ready to accept connections

看到了这个盒子就说明启动成功了,我们再打开一个终端,使用redis-cli启动客户端

$ ./redis-cli
127.0.0.1:6379>

可以看到进入了命令行的交互界面。 下面使用最简单的命令set laoxun redis,看到了服务端给我们返回的OK

127.0.0.1:6379> set laoxun redis
OK

再使用命令get laoxun,服务端把刚才我们设置的redis带着双引号"返回给了我们

127.0.0.1:6379> get laoxun
"redis"

通过上面的简单例子,就实现了客户端和服务端的两次通信(不包括连接),一次set一次get。 下面我们通过抓包工具wireshark看一下两次通信是怎么传输的。(关于wireshark怎么安装和使用,就不展开了,因为我也是边百度边学的)

set

抓包抓到4条TCP通信,有两条ACK忽略(其实是我不懂),主要看编号1和3的报文。

1 客户端发送给服务端

报文如下:

0000   02 00 00 00 45 00 00 58 00 00 40 00 40 06 00 00   ....E..X..@.@...
0010 7f 00 00 01 7f 00 00 01 c8 70 18 eb 90 e1 30 4b .........p....0K
0020 67 28 2a ec 80 18 17 cb fe 4c 00 00 01 01 08 0a g(*......L......
0030 3d 02 99 7e 3d 02 80 2c 2a 33 0d 0a 24 33 0d 0a =..~=..,*3..$3..
0040 73 65 74 0d 0a 24 36 0d 0a 6c 61 6f 78 75 6e 0d set..$6..laoxun.
0050 0a 24 35 0d 0a 72 65 64 69 73 0d 0a .$5..redis..

由于是TCP协议所以前面一半是TCP协议中的字段,从0030那一行的2a开始(含2a)往后才属于RESP协议,TCP协议的字段中的一部分我用不同颜色的框说明下深入学习redis(一)

  • 红色的框是127.0.0.1,前面的是代表 源ip,后面的是 目标ip。
  • 蓝色的框是51312,源端口号。
  • 绿色的框是6379,目标端口号。
  • 黄色的线后面就是TCP的data部分,就是redis对应的RESP的协议部分。

把黄线后的RESP整个取出来就是并通过查询ASCII转换为

2a 33 0d 0a 24 33 0d 0a 73 65 74 0d 0a 24 36 0d 0a 6c 61 6f 78 75 6e 0d 0a 24 35 0d 0a 72 65 64 69 73 0d 0a
* 3 $ 3 s e t $ 6 l a o x u n $ 5 r e d i s

其中出现最多的 其实是RESP中的结束符号,代表每一部分的结束。还有本例子中的*$则对应RESP中不同数据类型的前缀符号。RESP2中的只有5种数据类型他们的前缀分别是+,-,:,$,*,但是在RESP3中又多了很多诸如_,,,#,!,(等新的前缀。关于不同的数据类型,我下面会有专门介绍,这里混个脸熟就行。 接下来进入详细解释:

  • *代表的是 数组,后面跟的数字是代表数组中的元素个数,对应这里 set, laoxun, redis三个字符串。
  • $代表的是 多行字符串,后面跟的数字是代表字符串的长度。
    • set长度是 3
    • laoxun长度是 6
    • redis长度是 5
  • 还有就是在每一部分结束后都得跟上

是不是很简单~还要提一嘴就是客户端发送给服务端都是以*数组的编码形式。

3 服务端回复给客户端

报文如下:

0000   02 00 00 00 45 00 00 39 00 00 40 00 40 06 00 00   ....E..9..@.@...
0010 7f 00 00 01 7f 00 00 01 18 eb c8 70 67 28 2a ec ...........pg(*.
0020 90 e1 30 6f 80 18 18 e9 fe 2d 00 00 01 01 08 0a ..0o.....-......
0030 3d 02 99 7e 3d 02 99 7e 2b 4f 4b 0d 0a =..~=..~+OK..

0030那一行的2b开始(含2b)属于RESP协议深入学习redis(一)解释和前面的是一样的,其实可以看到就是前后互换了位置,因为本次报文是服务端响应给客户端,之后所有关于RESP的解释都不会再解释TCP的部分。

把黄线后的RESP整个取出来就是并通过查询ASCII转换为

2b 4f 4b 0d 0a
+ O K

可以看到set命令服务端的响应非常简单

  • +代表的是 单行字符串,通常用来作为简单的状态响应。

get

也是抓到了4条,我们关注的同样是1和3

1 客户端发送给服务端

报文如下:

0000   02 00 00 00 45 00 00 4d 00 00 40 00 40 06 00 00   ....E..M..@.@...
0010 7f 00 00 01 7f 00 00 01 c8 70 18 eb 90 e1 30 6f .........p....0o
0020 67 28 2a f1 80 18 17 cb fe 41 00 00 01 01 08 0a g(*......A......
0030 3d 67 71 15 3d 02 99 7e 2a 32 0d 0a 24 33 0d 0a =gq.=..~*2..$3..
0040 67 65 74 0d 0a 24 36 0d 0a 6c 61 6f 78 75 6e 0d get..$6..laoxun.
0050 0a .

RESP部分

2a 32 0d 0a 24 33 0d 0a 67 65 74 0d 0a 24 36 0d 0a 6c 61 6f 78 75 6e 0d 0a
* 2 $ 3 g e t $ 6 l a o x u n
  • *2此次客户端命令有两部分 getlaoxun组成
  • get长度是 3laoxun长度是 6

3 服务端回复给客户端

报文如下:

0000   02 00 00 00 45 00 00 3f 00 00 40 00 40 06 00 00   ....E..?..@.@...
0010 7f 00 00 01 7f 00 00 01 18 eb c8 70 67 28 2a f1 ...........pg(*.
0020 90 e1 30 88 80 18 18 e9 fe 33 00 00 01 01 08 0a ..0......3......
0030 3d 67 71 15 3d 67 71 15 24 35 0d 0a 72 65 64 69 =gq.=gq.$5..redi
0040 73 0d 0a s.. .

RESP部分,之后的报文不再展示TCP的部分了

24 35 0d 0a 72 65 64 69 73 0d 0a
$ 5 r e d i s
  • redis长度是 5,看到服务端并没有返回双引号 ",所以客户端控制台显示的 "应该是客户端为了友好展示加上的

至此,简单的HelloWorld的demo展示完了,我们了解到了RESP大概是什么样子的,所以下面准备好和我一起继续了解RESP了吗?

RESP3

以下的显示会消除字节之间的空格,更紧凑一些。 而且还需要说明的是,RESP36.0才加入的特性,但是为了保证兼容性,所以redis启动的时候默认还是用的RESP2,如果客户端需要启用RESP3则需要先使用6.0新加的hello命令,并把3作为第一个参数,像这样:

127.0.0.1:6379> hello 3
1# "server" => "redis"
2# "version" => "6.0.4"
3# "proto" => (integer) 3
4# "id" => (integer) 6
5# "mode" => "standalone"
6# "role" => "master"
7# "modules" => (empty array)

看到这个返回说明就设置成功了,此客户端之后发送的命令,服务端都会以RESP3的方式去返回,如果断开重连后,需要再次调用hello 3才能启用新的协议(同样的道理hello 2就可以切换至RESP2协议)。由于现在大部分用的还是6.0之前的版本,所以下面的部分demo命令会分别给出RESP2RESP3版本的返回。

$ Blob string

127.0.0.1:6379> set laoxun redis
OK
127.0.0.1:6379> get laoxun
"redis"
24350d0a72656469730d0a
$5 redis

+ Simple string

用来简单表示状态的返回。

127.0.0.1:6379> set laoxun redis
OK
2b4f4b0d0a
+OK

- Simple error

简单的错误返回,这里对一个字符串类型的value进行加1操作,就会返回异常。

127.0.0.1:6379> set laoxun redis
OK
127.0.0.1:6379> incr laoxun
(error) ERR value is not an integer or out of range

2d4552522076616c7565206973206e6f7420616e20696e7465676572206f72206f7574206f662072616e67650d0a
-ERR value is not an integer or out of range

: Number

整数返回

127.0.0.1:6379> set abc 1
OK
127.0.0.1:6379> incr abc
(integer) 2
3a320d0a
:2

_ Null

nil返回,可以看到RESP2是通过返回-1来当成nil的,客户端需要把接受到的-1处理成null(视不同的编程语言而定)

127.0.0.1:6379> getset abc 123
(nil)

RESP3

5f0d0a
_

RESP2

242d310d0a
$-1

, Double

RESP3新增的浮点数返回,RESP2是通过返回字符串来模拟浮点数返回的。

127.0.0.1:6379> zadd xjj 1.2 name
(integer) 1

RESP3

127.0.0.1:6379> zscore xjj name
(double) 1.2
2c312e320d0a
,1.2

RESP2

127.0.0.1:6379> ZSCORE xjj name
"1.2"
24330d0a312e320d0a
$3 1.2

# Boolean

TODO

! Blob error

TODO

= Verbatim string

RESP3新增的富文本字符串,在长度之后会跟着格式,如本例中的txt,另外还支持mkd表示markdown,再紧跟一个:,之后就和$类似就是文本的内容了。

127.0.0.1:6379> client list
id=4 addr=127.0.0.1:49665 fd=8 name= age=2328 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client user=default
id=5 addr=127.0.0.1:49777 fd=9 name= age=1663 idle=1653 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=zscore user=default

RESP3

3d3333300d0a7478743a69643d3420616464723d3132372e302e302e313a34393636352066643d38206e616d653d206167653d323332382069646c653d3020666c6167733d4e2064623d30207375623d3020707375623d30206d756c74693d2d3120716275663d323620716275662d667265653d3332373432206f626c3d30206f6c6c3d30206f6d656d3d30206576656e74733d7220636d643d636c69656e7420757365723d64656661756c740a69643d3520616464723d3132372e302e302e313a34393737372066643d39206e616d653d206167653d313636332069646c653d3136353320666c6167733d4e2064623d30207375623d3020707375623d30206d756c74693d2d3120716275663d3020716275662d667265653d30206f626c3d30206f6c6c3d30206f6d656d3d30206576656e74733d7220636d643d7a73636f726520757365723d64656661756c740a0d0a
=330 txt:id=4 addr=127.0.0.1:49665 fd=8 name= age=2328 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client user=default id=5 addr=127.0.0.1:49777 fd=9 name= age=1663 idle=1653 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=zscore user=default

RESP2

243332360d0a69643d3420616464723d3132372e302e302e313a34393636352066643d38206e616d653d206167653d333632332069646c653d3132393520666c6167733d4e2064623d30207375623d3020707375623d30206d756c74693d2d3120716275663d3020716275662d667265653d30206f626c3d30206f6c6c3d30206f6d656d3d30206576656e74733d7220636d643d636c69656e7420757365723d64656661756c740a69643d3520616464723d3132372e302e302e313a34393737372066643d39206e616d653d206167653d323935382069646c653d3120666c6167733d4e2064623d30207375623d3020707375623d30206d756c74693d2d3120716275663d323620716275662d667265653d3332373432206f626c3d30206f6c6c3d30206f6d656d3d30206576656e74733d7220636d643d636c69656e7420757365723d64656661756c740a0d0a
$326 id=4 addr=127.0.0.1:49665 fd=8 name= age=3623 idle=1295 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=client user=default id=5 addr=127.0.0.1:49777 fd=9 name= age=2958 idle=1 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client user=default

( Big number

TODO

* Array

127.0.0.1:6379> mset abc 123 def 456 ghi 789
OK
127.0.0.1:6379> mget abc def ghi
1) "123"
2) "456"
3) "789"
2a330d0a24330d0a3132330d0a24330d0a3435360d0a24330d0a3738390d0a
*3 $3 123 $3 456 $3 789

% Map

RESP3新增的类型,在此之前都是用Array来替代的。

127.0.0.1:6379> hset laoxun ab 123 cd 5678
(integer) 2

RESP3

127.0.0.1:6379> hgetall laoxun
1# "ab" => "123"
2# "cd" => "5678"
25320d0a24320d0a61620d0a24330d0a3132330d0a24320d0a63640d0a24340d0a353637380d0a
%2 $2 ab $3 123 $2 cd $4 5678

RESP2

127.0.0.1:6379> hgetall laoxun
1) "ab"
2) "123"
3) "cd"
4) "5678"
2a340d0a24320d0a61620d0a24330d0a3132330d0a24320d0a63640d0a24340d0a353637380d0a
*4 $2 ab $3 123 $2 cd $4 5678

~ Set

Map

127.0.0.1:6379> sadd abc 123 456 235 123
(integer) 3

RESP3

127.0.0.1:6379> sinter abc
1~ "123"
2~ "235"
3~ "456"
7e330d0a24330d0a3132330d0a24330d0a3233350d0a24330d0a3435360d0a
~3 $3 123 $3 235 $3 456

RESP2

127.0.0.1:6379> sinter abc
1) "123"
2) "235"
3) "456"
2a330d0a24330d0a3132330d0a24330d0a3233350d0a24330d0a3435360d0a
*3 $3 123 $3 235 $3 456

| Attribute

TODO

> Push

127.0.0.1:6379> publish abc hello
(integer) 0

RESP3

服务端正常返回了,但是客户端会报错直接就退出了,我感觉是redis-cli的bug

Reading messages... (press Ctrl-C to quit)
Error: Protocol error, got ">" as reply type byte
3e330d0a24390d0a7375627363726962650d0a24330d0a6162630d0a3a310d0a
>3 $9 subscribe $3 abc :1

RESP2

使用旧的协议则不会报错,该客户端就会阻塞在这里了,相当于消息队列中的消费者监听了

127.0.0.1:6379> SUBSCRIBE abc
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "abc"
3) (integer) 1

2a330d0a24390d0a7375627363726962650d0a24330d0a6162630d0a3a310d0a
*3 $9 subscribe $3 abc :1

还有4种前缀符号#,!,(,|,我试了很多命令都没能让服务端返回,暂时TODO下吧。

现在我们已经基本上把RESP协议都了解了一遍了,下一篇,我们从零开始自己实现一个简易版的redis client。

补充

Wireshark安装

如何抓本机的包

需要选择Loopback这个过滤器


以上是关于深入学习redis的主要内容,如果未能解决你的问题,请参考以下文章

深入学习Redis:主从复制

深入学习redis

深入学习Redis:Redis内存模型

深入学习Redis:持久化

深入学习Redis:集群

深入学习Redis:Redis内存模型