深入学习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-server
和redis-cli
分别对应服务端和客户端。 服务端一般启动后,会等待客户端的连接及连接后发送过来的命令,而且服务端和客户端通常都不会在同一台计算机上,就会涉及到网络通信,那么服务端和客户端是怎么通信的?这就会涉及另一个重要的概念就是协议
。常见的协议
有TCP
,HTTP
等,这里我不会展开(因为我压根也不懂啊),而redis中的协议
就是RESP
(REdis 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协议的字段中的一部分我用不同颜色的框说明下
-
红色的框是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
协议解释和前面的是一样的,其实可以看到就是前后互换了位置,因为本次报文是服务端响应给客户端,之后所有关于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
此次客户端命令有两部分get
和laoxun
组成 -
get
长度是3
,laoxun
长度是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
以下的显示会消除字节之间的空格,更紧凑一些。 而且还需要说明的是,RESP3
是6.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命令会分别给出RESP2
和RESP3
版本的返回。
$ 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的主要内容,如果未能解决你的问题,请参考以下文章