前言

为什么需要redis

redis非关系型的K-V数据库,数据存放内存了,当然肯定会序列化在磁盘,毕竟服务器关闭内存会清掉,内存大家都懂,快啊!所以Redis快啊。那我们可以单用Redis数据库存放我们的业务数据吗?可以,但是不推荐,为啥呢?我们在设计业务的时候,会设计各种关系表,A数据存A表,B数据存B表,C数据存C表,ABC表关联出我们需要的数据,方便管理,而Redis并不能做到这种,他只能K-V类型存储数据,并不能K和K关联去查询出数据,我说的不是代码去处理!!!所以,Redis主要是做缓存作用,放一些经常访问,不轻易变动,并访问次数多的数据,或者ABC表关联的查询慢的数据。

理论来说,不是因为Redis解决了某些业务,Redis能解决的业务,关系型数据库肯定能解决,换而言之,我们首先考虑使用关系型数据库解决问题,Redis干什么?Redis做的是加速做缓存,解决关系型数据库IO量大或访问慢的问题,从而加速业务的处理!

Redis介绍

Redis类型

redis是五种类型,类型说的是Value的类型,String、hashs、lists、sets、sorted set,redis为何存在着类型呢?方便与数据的分类管理,可以直接取出数据,那你说我Value存放Json类型的数据,分别对应redis的类型不也是可以的吗,这就涉及到另一个问题。

我的确存放Json类型就完事,客户端解析就好了,但是不方便啊,我问还要吧整个Json取出来解析,然后通过算法去找到我需要的数据,太麻烦了,并且Json数据很大呢?这个又牵扯到数据传输贷款问题。而redis直接做好了,直接方法取数据就行了。

eopll是什么(多路复用)

Redis单进程单线程,那他凭什么说它快啊,就因为他是内存处理的吗?我1w个并发来了,他就算内存处理的再快,他一个线程能处理过来吗?这不扯淡嘛!这个时候就要提一个东西了【eopll】

先看图,很复杂的东西,涉及到内核

image-20210509181216390

我们的程序是运行在JVM上的,clint连接是连接到内核上的,linux一起皆文件,那么程序是通过什么和内核连接进行IO操作呢?这个时候有一个概念【文件描述符】fd。

我们可以通过查找mysql的进程号,去他的程序文件的fd看他的文件描述符

1
ps -ef | grep mysql

image-20210509181936275

1
cd /proc/2745/fd

image-20210509182044115

看到前面的数字就是文件描述符的名称,我们针对这个逻辑,客户端连接mysql会发现多了一个fd socket连接

image-20210509182731204

刚才是没有53文件描述符的,现在多了一个53 socket文件描述符。文件描述符说完了,那么我们说回redis。

redis是单线程的,他怎么读取多个文件描述符呢?循环就完事了!循环文件描述符看有没有数据,有数据就拿过来处理,并且个拿过来处理是NIO非BIO,非阻塞的。

这里会出现另一个问题,我1000个fd,循环1000次还是慢啊,于是,内核出了一个select,select会直接存放有数据的fd,redis直接调用select方法,拿到是有数据的fd进行处理!

大量fd又会引发一个问题,数据copy问题啊,你传给我,我也要传回去啊,还是慢,于是内核又出了一个mmap,建立一个用户空间,一个红黑树链表,我把fd放到链表里不就可以了,内核去链表里read,应用也写到链表里,不就行了。

很复杂,理解的不算多深,还需要在努力。一句话epoll是同步、非阻塞、多路复用。

最后推导

根据上面的知识,我们推导了redis连接、处理数据。redis特性快、单线程,这个基于eopll,想要做到事物相关的东西,需要什么呢?举个例子,我添加一个数据,出问题了,在删除他,redis因为是单线程的,所以他是顺序执行的,所以推导出,必须是同一个fd socket操作,肯定是ok的。很复杂?其实就是每个连接的命令是顺序性的。假设,我们有两个客户端的fd socket连接,我A客户端发请求删除,B客户端发请求添加,假如A客户端先到,B客户后到,对A客户端来说,我没有删除成功。

但是,上面的eopll解决的是连接的问题,并不能说明他快啊。我们知道redis是基于内存的,redis会去寻址,就是数据的地址,内存的寻址时间是纳秒,也就是说,只有连接达到10w,才会是秒级的响应!!!又因为mapp的设计,线程拿到fd处理完毕后,马上从链表里拿到另一个fd进行处理,这个过程是很快的,所以可推导redis快。

还有内存是一个线型的物理空间。。。

Redis基本操作

安装Redis

首先,现在安装包,访问Redis官网,找到下载链接,下载即可

image-20210509191704730

解压

1
tar -zxvf redis-6.2.3.tar.gz -C ../modul/

查看文件结构

image-20210509192101951

我们可以直接读取README.md文件获取安装说明。文档告诉我们,可以直接编译程序

1
make

或者可以编译安装到我们指定的目录

1
make PREFIX=指定目录 install

或者直接通过他自带的工具utils安装到服务上

1
2
cd utils
./install_server.sh

注意编译需要安装gcc程序

1
yum -y install gcc

我们于是直接编译

image-20210509192956719

到这里就成功了。

然后进去src目录下就有了服务

1
cd src

image-20210509193113387

我们可以看到有个redis-server,直接执行就能看到服务启动了

image-20210509193217879

但是这样启动太麻烦了,我们想要安装到服务上。先安装到一个目录下

1
make install PREFIX=/usr/local/redis6

image-20210509193519899

去目录看是否安装成功

image-20210509193646025

安装成功后,需要配置环境变量

1
vi /etc/profile

image-20210509193848854

1
source /etc/profile

执行安装服务脚本

1
./install_server.sh

运行脚本install_server.sh可能会报如下错误:

1
2
This systems seems to use systemd.
Please take a look at the provided example service unit files in this directory, and adapt and install them. Sorry!

打开install_server.sh,注释掉下面的内容:

1
2
3
4
5
6
7
8
#_pid_1_exe="$(readlink -f /proc/1/exe)"
#if [ "${_pid_1_exe##*/}" = systemd ]
#then
# echo "This systems seems to use systemd."
# echo "Please take a look at the provided example service unit files in this directory, and adapt and install them. Sorry!"
# exit 1
#fi
#unset _pid_1_exe

image-20210509194647949

信息很好理解,懒得解释了。我们在查看

1
systemctl status redis_6379

就能看到redis服务了。

然后需要去配置开启远程服务

注释掉

1
#bind 127.0.0.1 -::1

protected-mode改成no

1
protected-mode no

重启即可。

String类型操作

基础操作

进入redis客户端

1
redis-cli

自动连接的是0库,和默认端口6379的redis,也可以连接指定的库,下面举例连接2库。redis是也是有库的属性存在的,十六个库,0~15

1
redis-cli -n 2

设置值

1
set k1 hello

获取值

1
get k1

清楚所有数据

1
FLUSHALL

对k1追加数据

1
APPEND k1 " world"

删除key

1
del k1

获取子串,-1是反向索引,就是-1是字符串最后一位

1
GETRANGE k1 0 -1

在一个value的X位置设置子串,6位置就是开始设置的地方

1
SETRANGE k1 6 world

目标k值为空才添加数据,就是只做add操作

1
set k1 hello nx

目标k值为空才添加数据,就是只做update操作

1
set k1 hello xx

对value加1

1
incr k2

对value加x

1
INCRBY k2 2

对value减1

1
DECR k2

对value减x

1
DECRBY k2 2

查看value长度

1
STRLEN k1

长度这里要扯下,他取是的哪个长度,是我字符的长度吗?这里做个试验,查看k1的长度

image-20210509215435940

k1是hello长度是5没有问题,查看k2的长度

image-20210509215605272

k2是6个1,属性是int,长度是6,也没问题啊,查看k3的长度

image-20210509215741841

这里就不对了,我存放是是【哈】中文,获取k3怎么是16进制的格式啊,长度怎么会是3啊。这个时候就涉及到一个东西,二进制安全

二进制安全

我们从可客户端向redis设置值的时候,其实传的是二进制,为什么?其实,是为了统一!编码有很多,我用utf-8编码传入,GBK编码解析到redis存入,肯定会出问题,所以,redis只存放二进制,传出去也只传二进制,解析在哪里,在客户端的编码!redis不用去担心编码问题,我存二进制关心你编码干啥玩意。所以,多人开发的时候客户端的编码一定要统一,否则可能会出问题,解析出来的数据不对。

故,推断出,redis的长度取的是字节数,utf-8一个汉字有三个字节,长度当然是三了。

BItMap

很重要的东西!!!先看命令setbit的说明

image-20210510225252991

可以看到说明。key要偏移的k,offset字节的偏移量,value具体偏移量的值,注意这个是2进制,要么是0要么是1。offset要解释下,这个很重要,我们知道redis存储的是字节,那么偏移量偏移的是什么?是字节位的二进制位的索引!!!,这个很重要啊。我们举例,先把redis都清空直接偏移

1
setbit k1 1 1

这个时候我们会有一个问题,k1的长度是什么,k1的值是什么,我们获取看看

image-20210510230933541

长度其实我们已经猜到了是1,为什么呢,以为redis是按字节算长度的,而值为什么是@的?,我们在推理下。

首先k1是没有值的,他的字节是什么,伪00000000,为什么说伪呢,因为实际他不是8个0,我们就把他看成8个0,我们往右偏移1位就是

1
0 1 0 0 0 0 0 0

那么为什么是@这个值呢,我们知道redis存放得是字节,但是展示的具体字节展示的值,于是,去ascll表看下

image-20210510231427736

我们看到确实是@这个对应的值,然后再去验证下,k1往右偏移7位,偏移值是1,我想看是不是A

1
setbit k1 7 1

image-20210510231704766

我们已经看到是就是A,然后我们验证长度推导,k1往右偏移9位,这个时候会知道他的是二进制格式会是什么

1
0 1 0 0 0 0 0 1   0 1 0 0 0 0 0 0

我猜他的值是A@,长度是2

image-20210510232321834

具体原因自己体会,下面是他的位函数操作。

我想找寻字节区间的第一个出现bit,注意,start和end是字节的索引

image-20210510233224442

1
2
3
4
5
127.0.0.1:6379> BITPOS k1 0 0 
(integer) 0
127.0.0.1:6379> BITPOS k1 1 0 0
(integer) 1
127.0.0.1:6379>

我们知道k1的值是0 1 0 0 0 0 0 1 0 1 0 0 0 0 0 0,那么字节的索引是0-0,就是第一个字节,那么字节的二进制的第一个出现的1的二进制的索引肯定是1了,下面继续。

我想统计二进制1在字节出现了几次

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> help bitcount

BITCOUNT key [start end]
summary: Count set bits in a string
since: 2.6.0
group: string

127.0.0.1:6379> bitcount k1 0 1
(integer) 3
127.0.0.1:6379>

我想对两个k做与、或、非运算BITOP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> setbit k1 1 1
(integer) 0
127.0.0.1:6379> setbit k1 7 1
(integer) 0
127.0.0.1:6379> get k1
"A"
127.0.0.1:6379> setbit k2 1 1
(integer) 0
127.0.0.1:6379> setbit k2 6 1
(integer) 0
127.0.0.1:6379> get k2
"B"
127.0.0.1:6379> bitop and k3 k1 k2
(integer) 1
127.0.0.1:6379> get k3
"@"

我做了什么?

  1. 清空redis
  2. 设置k1的值,他的二进制是0 1 0 0 0 0 0 1,值是A
  3. 设置k2的值,他的二进制是0 1 0 0 0 0 1 0,值是B
  4. 然后k1和k2做与运算,最终他是二进制是0 1 0 0 0 0 0 0,所以他的值是@

那么这个有什么用呢?需求来了:

需求1:用户系统,统计用户登录天数且窗口随机,一年365天,关系型数据统计太麻烦,我们可以把登录记录放到字节里,我们模拟下,有一个用户叫joy。

用户登录第1天登录

1
setbit joy 0 1

用户登录第2天登录

1
setbit joy 1 1

用户登录第365天登录

1
setbit joy 364 1

然后,我想查看你前八天登录天数

1
2
127.0.0.1:6379> BITCOUNT joy 0 0
(integer) 2

我想看最后八天登录天数

1
2
127.0.0.1:6379> BITCOUNT joy -2 -1
(integer) 1

需求2:我想知道某个时间窗口的活跃用户是多少数量?用户量100w,那这玩意怎么快速统计出来啊?很简单继续bit操作,举例:

2021-05-11 用户id为1的用户登录了

1
setbit 20210511 0 1

2021-05-11 用户id为2的用户登录了

1
setbit 20210511 1 1

2021-05-11 用户id为5的用户登录了

1
setbit 20210511 4 1

2021-05-11 用户id为1000000的用户登录了

1
setbit 20210511 1000000 1

我想知道2021-05-11的活跃用户数量,直接

1
bitcount 20210511 0 -1
1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> setbit 20210511 0 1
(integer) 0
127.0.0.1:6379> setbit 20210511 1 1
(integer) 0
127.0.0.1:6379> setbit 20210511 2 1
(integer) 0
127.0.0.1:6379> setbit 20210511 1000000 1
(integer) 0
127.0.0.1:6379> BITCOUNT 20210511 0 -1
(integer) 4

然后,我们这些存储占用了多少呢?我们来计算下,100w个bit位是125,000个字节,125000个字节是125k。。。我草,我用125k存放了100w个用户在20210511这天的登录状态!!!

list、hash、set、sorted_set类型操作

list类型操作

list感觉很重要,但是平时好像并未使用的多,错亿了。。。先看图

image-20210512211914682

我们可以使用help @list命令查看list命令,其他类型的也可以使用这个命令,命令太多,我就不展示了,下面进行实操

  • 添加数据直接lpush命令

    1
    2
    127.0.0.1:6379> LPUSH k1 a b c d e f g
    (integer) 7

    这个时候我们的脑海中应该会思考,添加进的数据顺序是什么?lpush这个l是什么意思,我们查看下数据

    1
    2
    3
    4
    5
    6
    7
    8
    127.0.0.1:6379> LRANGE k1 0 -1
    1) "g"
    2) "f"
    3) "e"
    4) "d"
    5) "c"
    6) "b"
    7) "a"

    哎,怎么是倒过来的啊,l就是左插入的意思!那么有L肯定会有R,直接操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    127.0.0.1:6379> RPUSH k2 a b c d e f g
    (integer) 7
    127.0.0.1:6379> LRANGE k2 0 -1
    1) "a"
    2) "b"
    3) "c"
    4) "d"
    5) "e"
    6) "f"
    7) "g"

    这个时候就正常了,其实list的数据结构是双向链表,如下图

    image-20210512212817011

key记录着头value和尾value,lpush就是在左边添加数据,r就是在后面添加数据。其实lpush添加的数据就像是栈,而rpush添加的数据就像是队列。

还有一种弹出数据的操作lpop,弹出左边的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> lpop k1
"g"
127.0.0.1:6379> lpop k1
"f"
127.0.0.1:6379> lpop k1
"e"
127.0.0.1:6379> lpop k1
"d"
127.0.0.1:6379> LRANGE k1 0 -1
1) "c"
2) "b"
3) "a"
127.0.0.1:6379>

还可以有个rpop,弹出右边的数据

1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:6379> LRANGE k1 0 -1
1) "c"
2) "b"
3) "a"
127.0.0.1:6379> rpop k1
"a"
127.0.0.1:6379> rpop k1
"b"
127.0.0.1:6379> rpop k1
"c"
127.0.0.1:6379>

根据索引取出元素lindex

1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> LRANGE k2 0 -1
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
6) "f"
7) "g"
127.0.0.1:6379> lindex k2 0
"a"
127.0.0.1:6379> lindex k2 1
"b"
127.0.0.1:6379>

改变索引的值lset

1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:6379> lset k2 1 xxxx
OK
127.0.0.1:6379> LRANGE k2 0 -1
1) "a"
2) "xxxx"
3) "c"
4) "d"
5) "e"
6) "f"
7) "g"
127.0.0.1:6379>

移除某个值lrem,从左边或者右边开始移除count个 的value值,count如果是正数,就是左边开始移除,如果为负数就是右边开始移除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
127.0.0.1:6379> LRANGE k3 0 -1
1) "5"
2) "k"
3) "h"
4) "3"
5) "n"
6) "1"
7) "b"
8) "1"
9) "3"
10) "2"
11) "b"
12) "b"
13) "b"
14) "b"
15) "1"
127.0.0.1:6379> LREM k3 2 b
(integer) 2
127.0.0.1:6379> LRANGE k3 0 -1
1) "5"
2) "k"
3) "h"
4) "3"
5) "n"
6) "1"
7) "1"
8) "3"
9) "2"
10) "b"
11) "b"
12) "b"
13) "1"
127.0.0.1:6379> LREM k3 -1 b
(integer) 1
127.0.0.1:6379> LRANGE k3 0 -1
1) "5"
2) "k"
3) "h"
4) "3"
5) "n"
6) "1"
7) "1"
8) "3"
9) "2"
10) "b"
11) "b"
12) "1"
127.0.0.1:6379>

统计多少个元素llen

1
2
127.0.0.1:6379> LLEN k3
(integer) 12

阻塞的弹出元素,不管你是是否有元素blpop,这个有点重要,可以做,timeout是最多阻塞时间

1
2
3
BLPOP key [key ...] timeout
summary: Remove and get the first element in a list, or block until one is available
since: 2.0.0

我们做个试验开一个窗口执行blpop命令,另一个窗口填数据

image-20210512215322132

一直 在阻塞,我们另一个窗口口添加元素,会发现直接执行成功,还有等待时间

image-20210512215433335

HASH操作

hash解决的是更深层次的数据结构封装,其实没什么好说的,像是map;

添加数据hset,查看数据hget,查看全部数据hgetall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
127.0.0.1:6379> hset lbw age 30
(integer) 1
127.0.0.1:6379> hset lbw sex 1
(integer) 1
127.0.0.1:6379> hset lbw sx nb
(integer) 1
127.0.0.1:6379> hget lbw age
"30"
127.0.0.1:6379> hget lbw sex
"1"
127.0.0.1:6379> HGETALL lbw
1) "age"
2) "30"
3) "sex"
4) "1"
5) "sx"
6) "nb"

批量添加数据hmset

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> hmset zmr a 1 b 2 c 3
OK
127.0.0.1:6379> HGETall zmr
1) "a"
2) "1"
3) "b"
4) "2"
5) "c"
6) "3"
127.0.0.1:6379>

对数值做计算,整形操作:hincrby,浮点型操作:hincrbyfloat,正数就是相加,负数就是相减,应用场景点赞收藏,详情页

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> hget zmr a
"1"
127.0.0.1:6379> HINCRBY zmr a 1
(integer) 2
127.0.0.1:6379> hget zmr a
"2"
127.0.0.1:6379> HINCRBYFLOAT zmr a 0.5
"2.5"
127.0.0.1:6379> hget zmr a
"2.5"
127.0.0.1:6379> HINCRBYFLOAT zmr a -0.5
"2"

SET操作

set是去重的一种集合,不维护排序的一种存储类型,和list区别是list是有序的,可重复的,注意list的有序是插入的顺序

image-20210512220823582

插入一批数据sadd

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> sadd k1 a b c d e f g a a a a a 
(integer) 7
127.0.0.1:6379> SMEMBERS k1
1) "b"
2) "d"
3) "a"
4) "c"
5) "g"
6) "f"
7) "e"

交集:SINTER,交集并存储到一个key:SINTERSTORE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
127.0.0.1:6379> sadd k2 1 2 3 4 5 6
(integer) 6
127.0.0.1:6379> sadd k3 2 3 4 5 6 7 8
(integer) 7
127.0.0.1:6379> SINTER k2 k3
1) "2"
2) "3"
3) "4"
4) "5"
5) "6"
127.0.0.1:6379> SINTERSTORE k4 k2 k3
(integer) 5
127.0.0.1:6379> SMEMBERS k4
1) "2"
2) "3"
3) "4"
4) "5"
5) "6"

并集:SUNION

1
2
3
4
5
6
7
8
9
127.0.0.1:6379> SUNION k2 k3
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"

差集:SDIFF

1
2
127.0.0.1:6379> SDIFF k2 k3
1) "1"

随机事件:SRANDMEMBER,随机取出count个数据,具体介绍看上面的图

1
2
3
4
5
6
7
8
9
127.0.0.1:6379> SRANDMEMBER k2 2
1) "5"
2) "4"
127.0.0.1:6379> SRANDMEMBER k2 2
1) "4"
2) "1"
127.0.0.1:6379> SRANDMEMBER k2 2
1) "6"
2) "3"

sorted_set

带顺序的set,其实是根据设置的数值排序,如果权重相等,那么根据名称排序

image-20210512222319108

添加数据,需要带上权重ZADD,可选择打印出权重值withscores

1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> ZADD k1 1 a 2 b 3 c
(integer) 3
127.0.0.1:6379> ZRANGE k1 0 -1
1) "a"
2) "b"
3) "c"
127.0.0.1:6379> ZRANGE k1 0 -1 withscores
1) "a"
2) "1"
3) "b"
4) "2"
5) "c"
6) "3"

查看权重值:ZSCORE,取出排名:ZRANk

1
2
3
4
5
6
127.0.0.1:6379> ZSCORE k1 a
"1"
127.0.0.1:6379> ZRANk k1 a
(integer) 0
127.0.0.1:6379> ZRANk k1 b
(integer) 1

数值计算:ZINCRBY,数值加的是权重,权重是不变的变化的

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> zadd k3 1 haha 2 heihei 3 gaga
(integer) 3
127.0.0.1:6379> ZRANGE k3 0 -1
1) "haha"
2) "heihei"
3) "gaga"
127.0.0.1:6379> ZINCRBY k3 3 haha
"4"
127.0.0.1:6379> ZRANGE k3 0 -1
1) "heihei"
2) "gaga"
3) "haha"

zset的交并集是要把权重相加的,为什么呢,因为涉及到排序

并集:ZUNIONSTORE,指定几个key参与交集,注意看权重,可以看到权重是相加的,并且排好序

1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> zadd k1 30 lbw  20 pdd 50 dsm
(integer) 3
127.0.0.1:6379> zadd k2 70 lbw 30 pdd 10 dsm
(integer) 3
127.0.0.1:6379> ZUNIONSTORE k3 2 k1 k2
(integer) 3
127.0.0.1:6379> ZRANGE k3 0 -1 withscores
1) "pdd"
2) "50"
3) "dsm"
4) "60"
5) "lbw"
6) "100"

还可以对权重做计算,加上权重,就是key乘后面的权重值,然后相加

1
2
3
4
5
6
7
8
9
127.0.0.1:6379> ZUNIONSTORE k4 2 k1 k2 weights 1 0.5
(integer) 3
127.0.0.1:6379> ZRANGE k4 0 -1 withscores
1) "pdd"
2) "35"
3) "dsm"
4) "55"
5) "lbw"
6) "65"

那么,重点来了sorted_set如何做到排序,并且查找插入高效呢?其实他的底层是一个skip list跳跃表,看下图

image-20210512230358058

跳跃表有一个随机造层的概念,就是把数据放到不同的层次,实现跳跃,我先从第一个数值开始插入,判断数值是否大于当前数值,如果不大于,就放到第一个,如果大于,就找后面的数值,如果为null就走第二层,第二层重复。我们模拟下。我先插入权重为20的。

  • 第一层:判断是否大于第一个节点,大于。判断是否大于第二个节点,大于。判断是否大于第三个节点,为nil,进入第二层判断
  • 第二层:判断是否大于第二个节点,大于。判断是否大于第三个节点,不大于。这个是否我们知道了我因放到第三个节点的前面,走下面的第三层
  • 第三层:判断是否大于掐面的节点,不大于,于是把节点放到22的前面,11的后面,然后开始维护层,把20放到第二层,又是新的一层。

简答的理解下,以后看是否能够手写一个跳跃表。

Redis技术的概念

redis自身会有一些概念性的东西,来帮助我们使用redis,如

  • 管道:批量的执行redis命令
  • 事物:开启事物来保证一些key的修改,但是redis的事物和关系型数据库又不同
  • 监控:和事物一起使用,监控key是否被修改,类似CAS
  • 等等。。。

我们可以访问官方网站 或者redis中文网站看这些概念,下面详细解释

管道

一次IO发送多次命令,批量的返回命令,省事省IO,就是多次请求,压缩成一笔请求。

安装nc

1
yum -y install nc

我们通过echo发送到nc

1
2
3
4
5
[root@node01 ~]# echo -e "set k2 10\nincr k2\nget k2" | nc localhost 6379
+OK
:11
$2
11

我们会发现第一次执行set是ok,k2加一了,get k2又获取到11的值,就是一次IO发送多次命令。那么就是压缩IO吗,有什么用吗,我们可以准备一些数据,在redis启动的时候加载一些数据。很好理解,可以看==>链接

image-20210519224321014

消息队列(Pub/Sub)

就是推送消息和收数据,还记得list的blpop一个阻塞的队列吗,这个是发布订阅,更加上层的的东西,我们可以用这个实现聊天室之类的东西。我们查看命令帮助

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
127.0.0.1:6379> help @pubsub

PSUBSCRIBE pattern [pattern ...]
summary: Listen for messages published to channels matching the given patterns
since: 2.0.0

PUBLISH channel message
summary: Post a message to a channel
since: 2.0.0

PUBSUB subcommand [argument [argument ...]]
summary: Inspect the state of the Pub/Sub subsystem
since: 2.8.0

PUNSUBSCRIBE [pattern [pattern ...]]
summary: Stop listening for messages posted to channels matching the given patterns
since: 2.0.0

SUBSCRIBE channel [channel ...]
summary: Listen for messages published to the given channels
since: 2.0.0

UNSUBSCRIBE [channel [channel ...]]
summary: Stop listening for messages posted to the given channels
since: 2.0.0

推送消息PUBLISH,第一个是k,第二个是value

1
2
3
127.0.0.1:6379> PUBLISH xx hello
(integer) 0
127.0.0.1:6379>

接收消息SUBSCRIBE,需要注意的是,我们无法接收开启接收消息之前的消息,这个很重要

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

当我们,需要做一个聊天系统的时候,可以使用pub/sub去实现,但是注意的是,这个无法存储信息,他只是一个订阅消息的功能,所以,我们想要需要把消息存储进来,那么存在一个问题,数据写到哪里呢?我们是否要把redis作为数据库呢?还是做成缓存?

image-20210519231002594

其实,数据还是要存在redis,所以redis做缓存,譬如3天之内的数据存放在redis,更老的数据存放在redis,这个时候有一个问题是,消息是顺序的,还要做窗口的控制,这个时候一个数据结构就出现了==>sorted_set,他会有两个命令

1
2
3
4
5
6
7
ZREMRANGEBYRANK key start stop
summary: Remove all members in a sorted set within the given indexes
since: 2.0.0

ZREMRANGEBYSCORE key min max
summary: Remove all members in a sorted set within the given scores
since: 1.2.0

ZREMRANGEBYRANK可以直接通过索引直接删除数据,达到了我们删除历史消息的效果。这个是存消息的方式和地方我们解决了,但是怎么存呢?

可以把消息发送到kafka,慢慢去消费存放到redis和数据库就完事了,还有一种开启两个消息队列,一个消息队列单独做发送到kafka里就行了。

事物

说起redis事物,好像很奇怪,大家好像都没提过,redis还有事物???没错,他有,但是他的事物不像redis那么完整,因为redis只有一个核心原则,那就是快!!!所以redis没有回滚这个概念,我们查看命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
127.0.0.1:6379> help @transactions

DISCARD -
summary: Discard all commands issued after MULTI
since: 2.0.0

EXEC -
summary: Execute all commands issued after MULTI
since: 1.2.0

MULTI -
summary: Mark the start of a transaction block
since: 1.2.0

UNWATCH -
summary: Forget about all watched keys
since: 2.2.0

WATCH key [key ...]
summary: Watch the given keys to determine execution of the MULTI/EXEC block

开启事物MULTI

这个就是打一个标志,我开启事物啦。因为redis是单进程的,所以redis是排队去执行的。EXEC是执行的事物

1
2
3
4
5
6
7
127.0.0.1:6379(TX)> set k1 1
QUEUED
127.0.0.1:6379(TX)> get k1
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) "1"

这个时候我们需要思考开启两个事物是什么情况,两个事物,一起开启一起执行,是哪个先执行,我们模拟一种情况,我们开启两个窗口同时开始事物

image-20210519231822789

客户端1执行

1
2
3
4
5
6
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 1
QUEUED
127.0.0.1:6379(TX)> INCR k1
QUEUED

客户端2执行

1
2
3
4
5
6
7
8
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> keys *
QUEUED
127.0.0.1:6379(TX)> set k1 99
QUEUED
127.0.0.1:6379(TX)> get k1
QUEUED

客户端2提交事物

1
2
3
4
5
127.0.0.1:6379(TX)> EXEC
1) (empty array)
2) OK
3) "99"
127.0.0.1:6379>

客户端1执行事物

1
2
3
4
5
6
127.0.0.1:6379(TX)> EXEC
1) OK
2) (integer) 2
127.0.0.1:6379> get k1
"2"
127.0.0.1:6379>

我们发现,他的事物的优先,是谁先提交谁先执行,不是谁先开启事物谁先执行,不会等待。

放弃事物DISCARD

1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:6379> set k2 1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR k2
QUEUED
127.0.0.1:6379(TX)> DISCARD
OK
127.0.0.1:6379> get k2
"1"
127.0.0.1:6379>

监控WATCH

就是监控一个key是否变化,如果变化了直接不执行命令,类似CAS。

客户端1执行

1
2
3
4
5
6
7
8
127.0.0.1:6379> set k3 1
OK
127.0.0.1:6379> WATCH k3
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR k3
QUEUED

客户端2修改k3的值

1
2
3
4
127.0.0.1:6379> set k3 100
OK
127.0.0.1:6379> get k3
"100"

客户端1执行事物,直接返回nil

1
2
3
4
127.0.0.1:6379(TX)> EXEC
(nil)
127.0.0.1:6379> get k3
"100"

RedisBloom模块

我们去redis下载他的模块给redis增加一些功能,比如,布隆过滤器,具体链接https://redis.io/modules。我们去https://github.com/RedisBloom/RedisBloom下载,RedisBloom的源码,上传到服务器里

image-20210519233824152

解压

1
unzip RedisBloom-master.zip -d ../modul/

编译,注意注意,最新版我编译失败。。。于是我找个2.2.0版本的就可以,不知道为什么,不管了

1
make

编译成功会出现一个so文件

image-20210519235055995

我们移到redis里面

1
cp redisbloom.so /usr/local/redis6/

然后需要停掉redis,因为我要加载模块

1
systemctl stop redis_6379

开启redis,并加载模块

1
redis-server --loadmodule /usr/local/redis6/redisbloom.so

这个时候我们进入redis

1
redis-cli

这个时候我们输入BF在按TABLE会自动提示命令。费了老大劲,这个布隆过滤器能干啥?还记得,我们上面的bitmap操作吗,位图,而布隆过滤器就是通过字节位数记录这个数据的标识

image-20210520001840044

通过哈希映射函数,映射到二进制的位数里就完事了。当然,肯定可能会有两个数据的一些函数的计算的位数相等,这个就是哈希碰撞。这个后面再说,当然会有一点可能一个变量和另一个变量的所有函数计算位数都是一样的,这个后面在研究,这个概率极低。

那么这个解决了什么事情呢?缓存穿透~假如我们有一个场景,先去redis找数据,如果没有就去数据库找,找完在存redis,然后有一人,不断的请求不存在的数据,我数据库也没有,那么他一直的请求,造成数据库的压力。

这个时候,我们把数据都存放在布隆里,先去bloom找如果没有,就直接返回就完事了,不需要再去查找数据了。这个时候,就算布隆这种形式的存储数据,可能会有误差,但是这种误差几率极小,解决的问题却很大,就算出问题,我们后端还是直接获取redis,还是会判断的,但是我们过滤了一大波的请求。

并且bloom会有下面三种实现

image-20210520002811535

布隆添加数据BF.ADD

1
2
127.0.0.1:6379> BF.ADD k1 haha
(integer) 1

判断是否存在BF.EXISTS

1
2
3
4
127.0.0.1:6379> BF.EXISTS k1 haha
(integer) 1
127.0.0.1:6379> BF.EXISTS k1 hah
(integer) 0

总结:

  1. 通过位图记录记录数据是否存在,在确定是否查询数据库
  2. 如果又穿透了,数据库并未有数据,设置一个key的标志,下次请求直接返回
  3. 数据库增加了元素,一定要对bloom添加元素。
  4. 写数据库又写redis,存在的数据一致性的问题,下面解决

redis的回收

我们知道redis的数据存放在内存里的,内存的大小是有限制的,存放的数据也是有限制了,所以,redis会自动的请求一些数据

image-20210520004556470

回收策略

当maxmemory限制达到的时候Redis会使用的行为由 Redis的maxmemory-policy配置指令来进行配置。

以下的策略是可用的:

  • noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
  • allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
  • volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
  • allkeys-random: 回收随机的键使得新添加的数据有空间存放。
  • volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
  • volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。

如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了。

选择正确的回收策略是非常重要的,这取决于你的应用的访问模式,不过你可以在运行时进行相关的策略调整,并且监控缓存命中率和没命中的次数,通过RedisINFO命令输出以便调优。

这个时候有一个概念,ttl过期的key

过期

设置k1过期时间为20秒

1
set k1 1 ex 20

使用ttl查看k1过期时间

1
2
3
4
5
6
127.0.0.1:6379> ttl k1
(integer) 17
127.0.0.1:6379> ttl k1
(integer) 15
127.0.0.1:6379> ttl k1
(integer) 14

当ttl过期了,在get就是nil,注意过期是-2,持久是-1

1
2
3
4
127.0.0.1:6379> ttl k1
(integer) -2
127.0.0.1:6379> get k1
(nil)

还可以先设置k在设置过期时间

1
2
3
4
5
6
127.0.0.1:6379> set k2 1
OK
127.0.0.1:6379> EXPIRE k2 50
(integer) 1
127.0.0.1:6379> ttl k2
(integer) 46

还可以设置一个

如果先设置一个过期的k1,在设置一个持久的k1,那么会直接覆盖

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> set k1 1
OK
127.0.0.1:6379> set k1 1 ex 20
OK
127.0.0.1:6379> ttl k1
(integer) 17
127.0.0.1:6379> set k1 2
OK
127.0.0.1:6379> ttl k1
(integer) -1

Redis如何淘汰过期的keys

Redis keys过期有两种方式:被动和主动方式。

当一些客户端尝试访问它时,key会被发现并主动的过期。

当然,这样是不够的,因为有些过期的keys,永远不会访问他们。 无论如何,这些keys应该过期,所以定时随机测试设置keys的过期时间。所有这些过期的keys将会从密钥空间删除。

具体就是Redis每秒10次做的事情:

  1. 测试随机的20个keys进行相关过期检测。
  2. 删除所有已经过期的keys。
  3. 如果有多于25%的keys过期,重复步奏1.

这是一个平凡的概率算法,基本上的假设是,我们的样本是这个密钥控件,并且我们不断重复过期检测,直到过期的keys的百分百低于25%,这意味着,在任何给定的时刻,最多会清除1/4的过期keys。

Redis的持久化

我们都知道Redis的数据是存放在内存里,内存有一个很重要的特性,就是掉电易失。所以,需要持久化到磁盘里,持久化有两个技术点,RDB、AOF

RDB

啥是RDB,就是把数据在某一个时间点或某个阈值触发了,存放到磁盘里,但是存放不是说存就存的,有两个概念

阻塞型持久

比如说,8点开始要把Redis数据持久到磁盘,我Redis停止服务(把操作放到队列),因为我要把8点这个维度之后的数据持久化到磁盘里,8点后的数据是不能持久的啊,不能这个事情没完了,你一直来数据我一直存储?所以,我们把Redis不在提供服务,我们把数据一点一点持久化到硬盘就行了,然后开启Redis服务。这个肯定是有问题的,生产环境服务是不能停止的,用户肯定骂娘。

非阻塞型持久

看下面的图

image-20210523114920130

我们先了解一个知识点,Linux的线程是隔离的,操作数据是映射的地址,有基础的同学马上就明白了,我把Redis数据的地址fork一份到一个子进程里,子进程慢慢持久化到磁盘不就好了?我们模拟下Linux数据的操作

模拟Linux数据操作

我们需要pstree了解linux的进程结构,安装程序

1
yum -y install psmisc

执行pstree命令,查看进程的分支情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@node01 ~]# pstree
systemd─┬─NetworkManager───2*[{NetworkManager}]
├─VGAuthService
├─agetty
├─auditd───{auditd}
├─chronyd
├─crond
├─dbus-daemon───{dbus-daemon}
├─lvmetad
├─master─┬─pickup
│ └─qmgr
├─polkitd───6*[{polkitd}]
├─redis-server───4*[{redis-server}]
├─rsyslogd───2*[{rsyslogd}]
├─sshd───sshd───bash───pstree
├─systemd-journal
├─systemd-logind
├─systemd-udevd
├─tuned───4*[{tuned}]
└─vmtoolsd───2*[{vmtoolsd}]

这要好像看不出来什么,我们执行命令/bin/bash继续查看进程结构,会发现├─sshd───sshd───bash───bash───pstree,这里多一个bash,这个就是进入子线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@node01 ~]# /bin/bash
[root@node01 ~]# pstree
systemd─┬─NetworkManager───2*[{NetworkManager}]
├─VGAuthService
├─agetty
├─auditd───{auditd}
├─chronyd
├─crond
├─dbus-daemon───{dbus-daemon}
├─lvmetad
├─master─┬─pickup
│ └─qmgr
├─polkitd───6*[{polkitd}]
├─redis-server───4*[{redis-server}]
├─rsyslogd───2*[{rsyslogd}]
├─sshd───sshd───bash───bash───pstree
├─systemd-journal
├─systemd-logind
├─systemd-udevd
├─tuned───4*[{tuned}]
└─vmtoolsd───2*[{vmtoolsd}]

知识你了解完了,我们在父线程创建一个变量,我们每次操作都要看进程,仔细看├─sshd───sshd这一行,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
systemd─┬─NetworkManager───2*[{NetworkManager}]
├─VGAuthService
├─agetty
├─anacron
├─auditd───{auditd}
├─chronyd
├─crond
├─dbus-daemon───{dbus-daemon}
├─lvmetad
├─master─┬─pickup
│ └─qmgr
├─polkitd───6*[{polkitd}]
├─redis-server───4*[{redis-server}]
├─rsyslogd───2*[{rsyslogd}]
├─sshd───sshd───bash───pstree
├─systemd-journal
├─systemd-logind
├─systemd-udevd
├─tuned───4*[{tuned}]
└─vmtoolsd───2*[{vmtoolsd}]
[root@node01 ~]# num=1
[root@node01 ~]# echo $num
1

进入bash的子进程查看是否可以看到父进程创建的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[root@node01 ~]# /bin/bash
[root@node01 ~]# pstree
systemd─┬─NetworkManager───2*[{NetworkManager}]
├─VGAuthService
├─agetty
├─anacron
├─auditd───{auditd}
├─chronyd
├─crond
├─dbus-daemon───{dbus-daemon}
├─lvmetad
├─master─┬─pickup
│ └─qmgr
├─polkitd───6*[{polkitd}]
├─redis-server───4*[{redis-server}]
├─rsyslogd───2*[{rsyslogd}]
├─sshd───sshd───bash───bash───pstree
├─systemd-journal
├─systemd-logind
├─systemd-udevd
├─tuned───4*[{tuned}]
└─vmtoolsd───2*[{vmtoolsd}]
[root@node01 ~]# echo $num

[root@node01 ~]#

我们会发现找不到父进程创建的变量,说明子父进程是隔离的,我们继续操作,退会父进程,把num设置为环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[root@node01 ~]# exit
exit
[root@node01 ~]# pstree
systemd─┬─NetworkManager───2*[{NetworkManager}]
├─VGAuthService
├─agetty
├─anacron
├─auditd───{auditd}
├─chronyd
├─crond
├─dbus-daemon───{dbus-daemon}
├─lvmetad
├─master─┬─pickup
│ └─qmgr
├─polkitd───6*[{polkitd}]
├─redis-server───4*[{redis-server}]
├─rsyslogd───2*[{rsyslogd}]
├─sshd───sshd───bash───pstree
├─systemd-journal
├─systemd-logind
├─systemd-udevd
├─tuned───4*[{tuned}]
└─vmtoolsd───2*[{vmtoolsd}]
[root@node01 ~]# echo $num
1
[root@node01 ~]# export num

在进入子进程看num的值是否能够取到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@node01 ~]# /bin/bash
[root@node01 ~]# pstree
systemd─┬─NetworkManager───2*[{NetworkManager}]
├─VGAuthService
├─agetty
├─anacron
├─auditd───{auditd}
├─chronyd
├─crond
├─dbus-daemon───{dbus-daemon}
├─lvmetad
├─master─┬─pickup
│ └─qmgr
├─polkitd───6*[{polkitd}]
├─redis-server───4*[{redis-server}]
├─rsyslogd───2*[{rsyslogd}]
├─sshd───sshd───bash───bash───pstree
├─systemd-journal
├─systemd-logind
├─systemd-udevd
├─tuned───4*[{tuned}]
└─vmtoolsd───2*[{vmtoolsd}]
[root@node01 ~]# echo $num
1

于是,是否能推断出,只有持有了数据的地址才能找到这个值,否者就是隔离了,但是又有一个问题,学过java 的朋友都知道,持有同一个地址的引用,我们去改变他的地址的值,对应引用的持有者的值都是改变的。这个时候,我们去改变子进程的值,看父进程的值是否会改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@node01 ~]# pstree
systemd─┬─NetworkManager───2*[{NetworkManager}]
├─VGAuthService
├─agetty
├─anacron
├─auditd───{auditd}
├─chronyd
├─crond
├─dbus-daemon───{dbus-daemon}
├─lvmetad
├─master─┬─pickup
│ └─qmgr
├─polkitd───6*[{polkitd}]
├─redis-server───4*[{redis-server}]
├─rsyslogd───2*[{rsyslogd}]
├─sshd───sshd───bash───bash───pstree
├─systemd-journal
├─systemd-logind
├─systemd-udevd
├─tuned───4*[{tuned}]
└─vmtoolsd───2*[{vmtoolsd}]
[root@node01 ~]# num =999
[root@node01 ~]# echo $num
999

查看父进程的值,是否改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[root@node01 ~]# exit
exit
[root@node01 ~]# pstree
systemd─┬─NetworkManager───2*[{NetworkManager}]
├─VGAuthService
├─agetty
├─anacron
├─auditd───{auditd}
├─chronyd
├─crond
├─dbus-daemon───{dbus-daemon}
├─lvmetad
├─master─┬─pickup
│ └─qmgr
├─polkitd───6*[{polkitd}]
├─redis-server───4*[{redis-server}]
├─rsyslogd───2*[{rsyslogd}]
├─sshd───sshd───bash───pstree
├─systemd-journal
├─systemd-logind
├─systemd-udevd
├─tuned───4*[{tuned}]
└─vmtoolsd───2*[{vmtoolsd}]
[root@node01 ~]# echo $num
1

父进程的值是没法被改变的,这个时候,我们又推断出一个东西,当一个进程改变值的时候,他对应的地址也会变。聪明的朋友已经明白了,Redis把数据地址复制到子进程,子进程慢慢的持久化不就完事了,父进程改变值并不会对子进程的引用有影响。

linux有个fork()函数,就是干这个事情的

image-20210523121717035

Redis RDB配置

我们查看Redis的配置,看他配置的持久化,我们找到配置的SNAPSHOTTING

持久化规则

你可以设置60秒判断数据是否够10000,那就进行持久操作,300秒数据是否到100那就持久操作,当然也可以设置个空,关闭持久

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Snapshotting can be completely disabled with a single empty string argument
# as in following example:
#
# save ""
#
# Unless specified otherwise, by default Redis will save the DB:
# * After 3600 seconds (an hour) if at least 1 key changed
# * After 300 seconds (5 minutes) if at least 100 keys changed
# * After 60 seconds if at least 10000 keys changed
#
# You can set these explicitly by uncommenting the three following lines.
#
# save 3600 1
# save 300 100
# save 60 10000

设置持久化的db文件名称

1
2
# The filename where to dump the DB
dbfilename dump.rdb

设置持久化目录

1
2
# Note that you must specify a directory here, not a file name.
dir /var/lib/redis/6379

是否开启压缩

1
2
3
4
5
# Compress string objects using LZF when dump .rdb databases?
# By default compression is enabled as it's almost always a win.
# If you want to save some CPU in the saving child set it to 'no' but
# the dataset will likely be bigger if you have compressible values or keys.
rdbcompression yes

是否开启校验位

1
2
3
# RDB files created with checksum disabled have a checksum of zero that will
# tell the loading code to skip the check.
rdbchecksum yes

RDB弊端

这样就完事了吗?不,RDB是有点弊端的。

  • 我们每进行一次持久化,持久化的是全量的库,哪怕之前的数据没有被修改
  • 不支持拉链,只有一个持久DB文件,如果这个DB文件丢了,就很麻烦
  • 丢失的数据可能稍微多点,因为并不是实时的持久,是点与点之间的持久
  • 永远的记住Redis是内存库,内存数据断电啥都没了,所以Redis掉电易失

所以AOF概念就出来了

AOF(append only mode)

一项技术概念被发明出来,肯定是为了解决问题,当我们清楚RDB的优势和弊端之后,于是,AOF就出来了。日志!每次操作都会记录到日志里(不断的append),这样我们就能非常的清楚我们进行哪些操作,就算突然断电,我们有日志还是能恢复数据。当然,既然有优势、肯定有弊端

image-20210523201936036

AOF弊端:

  • 既然是记录日志的操作,肯定是和磁盘IO关联上的,这个无法避免的,你内存库和磁盘有关联肯定会慢
  • 体量大,我10年的Redis操作,日志达到20G,其实就做了什么?set k1 set k2….就这两次操作,重复十年,但是我日志这么大,我要恢复数据,这要恢复到什么时候?

解决弊端:

其实上面的图,已经说明了,我们可以同时开启RDB和AOF,这个是Redis4.0后的机制,某个时刻,把那个时间段的数据存储成RDB到日志文件中,剩下的日志操作还是不断的append,不断的重复,所以AOF文件的体量和需要回复的操作就变小了。

我们永远的记住,AOP是不会修改文件,他只会不断的追加Redis操作。所以,当我们RDB和AOF同时开启的时候,只会有AOF文件,不会有RDB文件。我们通过配置文件来学习AOF

配置文件:

我们找到

1
############################## APPEND ONLY MODE ###############################

AOF默认关闭的,我们手动给打开

1
appendonly yes

起名字

1
2
# The name of the append only file (default: "appendonly.aof")
appendfilename "appendonly.aof"

append刷写磁盘级别,这因为要染指IO,所以这个会设置级别

everysec:每秒刷写磁盘

always:每次操作都刷写磁盘

no:是否每次追加完毕之后,都刷写磁盘。

这个就有意思了,当我们去磁盘写文件的时候,操作完毕都要进行一次flush,这样才能写入磁盘。其实,我们写入磁盘的数据,是写入一个buffer,当buffer满的时候,linux内核会自动调用刷写入磁盘,所以为no就是让他自己等buffer满了,自动刷进去磁盘,所以,这个级别可能会丢失一个小于buffer的数据。

1
2
3
4
5
# If unsure, use "everysec".

# appendfsync always
appendfsync everysec
# appendfsync no

如何Redis抛出一个子进程,在进行RDB或者一个AOF操作,父进程不会调用flush操作

1
2
3
4
# If you have latency problems turn this to "yes". Otherwise leave it as
# "no" that is the safest pick from the point of view of durability.

no-appendfsync-on-rewrite no

进行检查

1
aof-load-truncated yes

是否开启AOF和RDB混合操作,就是在进行重写AOF的文件,是否把RDB写入AOF文件

1
2
3
4
# When loading, Redis recognizes that the AOF file starts with the "REDIS"
# string and loads the prefixed RDB file, then continues loading the AOF
# tail.
aof-use-rdb-preamble yes

配置文件就这么多,我们会发现没多少啊,我们直接来操作

具体操作:

我们验证下我们的结论观点,我们做准备工作,为了更加清楚的操作,我们把Redis配置文件复制出来,我们在opt洗新建个测试目录,并把配置文件复制进来

1
2
3
4
5
6
[root@node01 ~]# cd /opt
[root@node01 opt]# mkdir test
[root@node01 opt]# cd test/
[root@node01 test]# mkdir redis
[root@node01 test]# cd redis/
[root@node01 redis]# cp /etc/redis/6379.conf ./

我们需要看前台日志,所以不让他是后台服务运行

1
daemonize no

关闭日志文件,我们想要日志文件打到屏幕里

1
#logfile /var/log/redis_6379.log

我们先把混合关闭

1
aof-use-rdb-preamble no

清除持久化文件

1
2
[root@node01 redis]# cd /var/lib/redis/6379
[root@node01 6379]# rm -rf dump.rdb

启动Redis

1
redis-server /opt/test/redis/6379.conf

我们另开窗口看Redis持久文件,会发现多一个AOF文件

1
2
3
4
[root@node01 ~]# cd /var/lib/redis/6379/
[root@node01 6379]# ll
总用量 0
-rw-r--r--. 1 root root 0 5月 31 21:35 appendonly.aof

我们查看文件内容,会发现什么都没有,我们开始向Redis写入一个hello,然后在查看文件

1
2
3
4
5
6
7
8
9
10
11
12
*2
$6
SELECT
$1
0
*3
$3
set
$2
k1
$5
hello

这个文件是Redis的日志规则,我们大致能看到是0号库,set k1 value是hello。

我们进行RDB持久操作,主要save是前台同步持久操作,bgsave是后台

1
2
127.0.0.1:6379> bgsave
Background saving started

我们在查看前台日志,解释是

  • 准备连接
  • 子进程pid 1895
  • 做DB写入磁盘
  • 0号库的 copy-on-write
  • 最后写入完成
1
2
3
4
5
1855:M 31 May 2021 21:35:44.338 * Ready to accept connections
1855:M 31 May 2021 21:43:16.016 * Background saving started by pid 1895
1895:C 31 May 2021 21:43:16.033 * DB saved on disk
1895:C 31 May 2021 21:43:16.033 * RDB: 0 MB of memory used by copy-on-write
1855:M 31 May 2021 21:43:16.070 * Background saving terminated with success

我们会发现/var/lib/redis/6379多一个RDB文件,我们查看这个文件,好家伙二进制看不懂,但是我们看到一个hello,嘿嘿

1
2
REDIS0009ú      redis-ver^E6.2.3ú
redis-bitsÀ@ú^EctimeÂôç´`ú^Hused-memÂPN^M^@ú^Laof-preambleÀ^@þ^@û^A^@^@^Bk1^EhelloÿåK²§éÌst

下面测试AOF重写功能,我们不断的set k1

1
2
3
4
5
6
127.0.0.1:6379> set k1 111
OK
127.0.0.1:6379> set k1 11
OK
127.0.0.1:6379> set k1 1
OK

我们查看AOF文件,会发现k1的操作日志很多笔,我们执行重写命令

1
2
127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started

会发现只剩一笔k1的操作

1
2
3
4
5
6
7
8
9
10
11
12
*2
$6
SELECT
$1
0
*3
$3
SET
$2
k1
$1
1

下面测试新版本,我们把混合开关打开,并把持久文件关掉

1
aof-use-rdb-preamble yes

我们再次启动redis,/var/lib/redis/6379,同样会多一个aof文件,我们进行set操作

1
2
3
4
5
6
7
8
127.0.0.1:6379> set k1 1
OK
127.0.0.1:6379> set k1 2
OK
127.0.0.1:6379> set k1 3
OK
127.0.0.1:6379> set k1 4
OK

当我们再次查看AOF文件,会发现一样的日志文件,但是,当执行BGREWRITEAOF在查看aof文件

1
2
REDIS0009ú      redis-ver^E6.2.3ú
redis-bitsÀ@ú^EctimeÂ!ë´`ú^Hused-memÂ(N^M^@ú^Laof-preambleÀ^Aþ^@û^A^@^@^Bk1À^DÿDÁ1ËÌ^X<8a>*

会发现AOF是RDB的二进制文件,然后我们在进行set操作,会发现,下面的文件是明文的方式追加的文件,他用^M区分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
REDIS0009ú      redis-ver^E6.2.3ú
redis-bitsÀ@ú^EctimeÂeë´`ú^Hused-memÂÀM^M^@ú^Laof-preambleÀ^Aþ^@û^A^@^@^Bk1À^Dÿ¹:^RÝÍ<8f>Ûz*2^M
$6^M
SELECT^M
$1^M
0^M
*3^M
$3^M
set^M
$2^M
k1^M
$1^M
4^MREDIS0009ú redis-ver^E6.2.3ú
redis-bitsÀ@ú^EctimeÂeë´`ú^Hused-memÂÀM^M^@ú^Laof-preambleÀ^Aþ^@û^A^@^@^Bk1À^Dÿ¹:^RÝÍ<8f>Ûz*2^M
$6^M
SELECT^M
$1^M
0^M
*3^M
$3^M
set^M
$2^M
k1^M
$1^M
4^M

然后,我们同时执行BGREWRITEAOFBGSAVE,会发现这两个文件大小是一样的,内容也是一样的

image-20210531220023524

以上就是RDB和AOF的内容,其实就是为了把Redis内存里的数据持久化到磁盘里。解决了持久化的问题,下面开始解决单机的问题。

Redis分布式

先说下AKF概念:

AKF 立方体也叫做scala cube,它在《The Art of Scalability》一书中被首次提出,旨在提供一个系统化的扩展思路。AKF 把系统扩展分为以下三个维度:

  • X 轴:直接水平复制应用进程来扩展系统。
  • Y 轴:将功能拆分出来扩展系统。
  • Z 轴:基于用户信息扩展系统。

主从和主备(X轴)

单机的Redis肯定会出问题,这个是毋庸置疑的,具体为什么我懒的解释,所以,需要多台Redis结合起来,我挂了宁给爷顶上去。而,顶上去这个概念分两种顶。

  • 主备:主和备数据是同步的,客户端只能访问主,主挂了,我备机直接顶上去,至于如何顶上去,我下面再说
  • 主从:主和从的数据是同步的,但是,从服务是可以协干活的,主挂了从也可以顶上去。何为从服务协助干活,就是主可以只接收写,从接收读,读写分里,但是这个又涉及到数据的一直性,我们下面再聊。

我上面所说的,主挂了备要顶上去,一定一定要注意一件事,主备数据一定要同步,就是强一致性,我们如果要保证这种强一致性关系,要么人工代码处理,要么引入第三方中间件,下面说。

功能存储(Y轴)

上面解决了单机的问题,但是没有解决存储的问题,我们可以基于业务去把数据放到不同的Redis库,客户端根据逻辑去区分使用哪个redis库。

分片存储(Z轴)

当Y轴的数据特别大的时候,我们可以按照规则去拆分数据,譬如,我A-Redis,数据达到了10G,我可以根据我设计的规则,把10G拆成两个5G。所以说是先拆分业务,在分片数据。

明白上面的思想之后,看下面的图就明白了了

image-20210603201319232

一致性

当我们理解上面的东西,从而去思考一个问题,我想要解决单点故障、容量、压力问题,从而带来了是技术的风险,那么有什么风险?数据同步问题!我想要使用主从,肯定要保证我的备机的数据和主机的数据是一致,但凡涉及到两个Redis数据一致的问题,就有三个概念,强一致性、弱一致性、最终一致性。

  • 强一致性:clint向Redis设置一个值,他的备机全部同步完成,才会返回成功,否则这个值无法访问

image-20210603202710571

  • 弱一致性:clint向Redis设置一个值,他的备机不管是否同步完成,都返回成功,都可以使用

image-20210603202729352

  • 最终一致性:clint向Redis设置一个值,拿着第三方的工具,如kafka放入这笔记录,然后去同步备机,最终主备数据一致

image-20210603202749272

所以需要明确,两个Redis主备是否能够容忍数据不一致?当然不行 了。我A-Redis设置一致值,B-Redis查询,却查不到,这个当然无法容忍。所以,就剩下强一致性和最终一致性,强一致性当然没问题,但是强一致性,必然会导致设置数据的风险和响应,而最终一致性会导致,主从模式下,备机的读数据不是最新的。所以,最终一致性也差不多可以说成强一致性。

监控

当我对Redis做主备,客户端访问的是主,当主挂掉备机顶上,做主从,客户端可以访问的是主或者备机。其实,已经发现了,主挂掉了,备机怎么顶上呢?或者说,我们怎么知道主机挂了啊。这个时候监控的概念就出来了,监控主是否挂掉,主挂掉了,程序处理备机顶上。然后搞笑的来了,我监控的程序,也是个程序了,也会挂啊,然后监控程序的HA来了,就是多台监控程序,做高可用。

而监控配置多个还有一个好处,因为无法保证监控程序和Redis的网络是否一直是畅通的,万一单机的监控程序突然网络不通了,一看唉,Redis挂了,我感觉让备机顶上去,这样当然不行了,而多个监控程序,可以进行投票,当票数大部分说主没有挂,小部分说主挂了,这个时候可以判断出主并没有事情。

这里还有一个小细节,监控程序的数量建议是奇数个,偶数个的风险会比奇数个大,多一个服务器嘛,自己去理解。

CAP原则

CAP原则又称CAP定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。

一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)

可用性(A):保证每个请求不管成功或者失败都有响应。

分区容忍性(P):系统中任意信息的丢失或失败不会影响系统的继续运作。 [1]

CAP原则的精髓就是要么AP,要么CP,要么AC,但是不存在CAP。如果在某个分布式系统中数据无副本, 那么系统必然满足强一致性条件, 因为只有独一数据,不会出现数据不一致的情况,此时C和P两要素具备,但是如果系统发生了网络分区状况或者宕机,必然导致某些数据不可以访问,此时可用性条件就不能被满足,即在此情况下获得了CP系统,但是CAP不可同时满足 [2] 。

因此在进行分布式架构设计时,必须做出取舍。当前一般是通过分布式缓存中各节点的最终一致性来提高系统的性能,通过使用多节点之间的数据异步复制技术来实现集群化的数据一致性。通常使用类似 memcached 之类的 NOSQL 作为实现手段。虽然 memcached 也可以是分布式集群环境的,但是对于一份数据来说,它总是存储在某一台 memcached 服务器上。如果发生网络故障或是服务器死机,则存储在这台服务器上的所有数据都将不可访问。由于数据是存储在内存中的,重启服务器,将导致数据全部丢失。当然也可以自己实现一套机制,用来在分布式 memcached 之间进行数据的同步和持久化,但是实现难度是非常大的 [3] 。

主从设置(X)

说了一大堆,开始实操,先从主从设置开始

准备三个配置文件,首先安装redis服务

1
cd /opt/modul/redis-6.2.3/utils/

执行安装两个服务,端口6380、6381

1
./install_server.sh

在opt目录创建测试目录,并把配置文件复制到测试目录里

1
2
3
4
5
6
7
8
9
[root@node01 utils]# cd /opt
[root@node01 opt]# mkdir redisTest
[root@node01 opt]# cd redisTest/
[root@node01 redisTest]# cp /etc/redis/* ./
[root@node01 redisTest]# ll
总用量 276
-rw-r--r--. 1 root root 93795 6月 3 21:18 6379.conf
-rw-r--r--. 1 root root 93800 6月 3 21:18 6380.conf
-rw-r--r--. 1 root root 93800 6月 3 21:18 6381.conf

修改配置文件不要后台服务器式启动,关闭日志文件、关闭aof,我们需要查看台的日志

1
2
3
4
5
daemonize no

#logfile /var/log/redis_6380.log

appendonly no

删除所有redis持久化文件

1
2
3
4
5
6
7
8
9
[root@node01 redisTest]# cd /var/lib/redis/
[root@node01 redis]# ll
总用量 0
drwxr-xr-x. 2 root root 44 5月 31 21:59 6379
drwxr-xr-x. 2 root root 6 6月 3 21:16 6380
drwxr-xr-x. 2 root root 6 6月 3 21:17 6381
[root@node01 redis]# rm -rf 6379/*
[root@node01 redis]# rm -rf 6380/*
[root@node01 redis]# rm -rf 6381/*

开三个窗口启动redis

1
2
3
redis-server 6379.conf
redis-server 6380.conf
redis-server 6381.conf

在开三个窗口登录验证是否启动成功

1
2
3
redis-cli -p 6379
redis-cli -p 6380
redis-cli -p 6381

接下来了解SLAVEOF,就是使用REPLICAOF命令让从服务器追随主服务器

1
2
3
4
5
6
127.0.0.1:6379> help SLAVEOF

SLAVEOF host port
summary: Make the server a replica of another instance, or promote it as master. Deprecated starting with Redis 5. Use REPLICAOF instead.
since: 1.0.0
group: server

在6380和6381追随6379

1
REPLICAOF 127.0.0.1 6379

马上查看6379的日志,他说的是什么呢?就是6380追随了我,我给6380同步数据,同步数据是copy-on-write

1
2
3
4
5
6
7
8
9
10
2657:M 03 Jun 2021 21:31:49.179 * Ready to accept connections
^L2657:M 03 Jun 2021 21:38:20.291 * Replica 127.0.0.1:6380 asks for synchronization
2657:M 03 Jun 2021 21:38:20.291 * Partial resynchronization not accepted: Replication ID mismatch (Replica asked for '662b560c4a5ad29ef05197c8e67f3fbe4f15ca3c', my replication IDs are 'e3df3e2f733efbede14e2a7ced1bd2700a3213f6' and '0000000000000000000000000000000000000000')
2657:M 03 Jun 2021 21:38:20.291 * Replication backlog created, my new replication IDs are 'ef2b0e497b3f254efc838d9154dc5542c467eb32' and '0000000000000000000000000000000000000000'
2657:M 03 Jun 2021 21:38:20.291 * Starting BGSAVE for SYNC with target: disk
2657:M 03 Jun 2021 21:38:20.292 * Background saving started by pid 2752
2752:C 03 Jun 2021 21:38:20.303 * DB saved on disk
2752:C 03 Jun 2021 21:38:20.304 * RDB: 0 MB of memory used by copy-on-write
2657:M 03 Jun 2021 21:38:20.364 * Background saving terminated with success
2657:M 03 Jun 2021 21:38:20.364 * Synchronization with replica 127.0.0.1:6380 succeeded

然后去查看6380的日志,大致意思是说,我追随了6379,然后同步数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2693:S 03 Jun 2021 21:38:20.291 * Connecting to MASTER 127.0.0.1:6379
2693:S 03 Jun 2021 21:38:20.291 * MASTER <-> REPLICA sync started
2693:S 03 Jun 2021 21:38:20.291 * REPLICAOF 127.0.0.1:6379 enabled (user request from 'id=3 addr=127.0.0.1:36048 laddr=127.0.0.1:6380 fd=8 name= age=295 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=44 qbuf-free=40910 argv-mem=22 obl=0 oll=0 omem=0 tot-mem=61486 events=r cmd=replicaof user=default redir=-1')
2693:S 03 Jun 2021 21:38:20.291 * Non blocking connect for SYNC fired the event.
2693:S 03 Jun 2021 21:38:20.291 * Master replied to PING, replication can continue...
2693:S 03 Jun 2021 21:38:20.291 * Trying a partial resynchronization (request 662b560c4a5ad29ef05197c8e67f3fbe4f15ca3c:1).
2693:S 03 Jun 2021 21:38:20.303 * Full resync from master: ef2b0e497b3f254efc838d9154dc5542c467eb32:0
2693:S 03 Jun 2021 21:38:20.303 * Discarding previously cached master state.
2693:S 03 Jun 2021 21:38:20.364 * MASTER <-> REPLICA sync: receiving 175 bytes from master to disk
2693:S 03 Jun 2021 21:38:20.364 * MASTER <-> REPLICA sync: Flushing old data
2693:S 03 Jun 2021 21:38:20.364 * MASTER <-> REPLICA sync: Loading DB in memory
2693:S 03 Jun 2021 21:38:20.365 * Loading RDB produced by version 6.2.3
2693:S 03 Jun 2021 21:38:20.365 * RDB age 0 seconds
2693:S 03 Jun 2021 21:38:20.365 * RDB memory usage when created 1.85 Mb
2693:S 03 Jun 2021 21:38:20.365 * MASTER <-> REPLICA sync: Finished with success

向主设置k1,查看从是否同步

1
2
3
4
5
6
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set k1 hello
OK
127.0.0.1:6379> get k1
"hello"

查看6380,没问题都同步过去了

1
2
127.0.0.1:6380> get k1
"hello"

接下来先不让6381设置追随主,先设置k1为1,在追随6379,在查看k1,这个时候肯定是hello,因为同步过去了

1
2
3
4
5
6
7
8
127.0.0.1:6381> set k1 1
OK
127.0.0.1:6381> get k1
"1"
127.0.0.1:6381> REPLICAOF 127.0.0.1 6379
OK
127.0.0.1:6381> get k1
"hello"

如果从挂了,主又有了数据会发生什么情况呢,我们先手动ctrl+c关闭6380,在主设置一些数据,在6380追随主

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> set k2 2
OK
127.0.0.1:6379> set k3 2
OK
127.0.0.1:6379> set k3 4452
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k1"
3) "k2"

启动6380查看k

1
2
127.0.0.1:6380> keys *
1) "k1"

在重新追随主,发现数据都同步了

1
2
3
4
5
6
127.0.0.1:6380> REPLICAOF 127.0.0.1 6379
OK
127.0.0.1:6380> keys *
1) "k1"
2) "k3"
3) "k2"

那主挂了呢?直接手动ctrl+c关闭主,会发现从疯狂的报错

1
2
3
4
5
6
7
8
9
10
6:S 03 Jun 2021 21:59:52.139 # Error condition on socket for SYNC: Connection refused
2756:S 03 Jun 2021 21:59:52.550 * Connecting to MASTER 127.0.0.1:6379
2756:S 03 Jun 2021 21:59:52.551 * MASTER <-> REPLICA sync started
2756:S 03 Jun 2021 21:59:52.551 # Error condition on socket for SYNC: Connection refused
2756:S 03 Jun 2021 21:59:53.579 * Connecting to MASTER 127.0.0.1:6379
2756:S 03 Jun 2021 21:59:53.579 * MASTER <-> REPLICA sync started
2756:S 03 Jun 2021 21:59:53.579 # Error condition on socket for SYNC: Connection refused
2756:S 03 Jun 2021 21:59:54.608 * Connecting to MASTER 127.0.0.1:6379
2756:S 03 Jun 2021 21:59:54.608 * MASTER <-> REPLICA sync started
2756:S 03 Jun 2021 21:59:54.608 # Error condition on socket for SYNC: Connection refused

这个时候希望切换主服务,于是先把6380从的模式关闭

1
2
127.0.0.1:6380> REPLICAOF no one
OK

在让6381追随6380,发现都是ok的。

1
2
3
4
5
6
127.0.0.1:6381> REPLICAOF 127.0.0.1 6380
OK
127.0.0.1:6381> keys *
1) "k3"
2) "k1"
3) "k2"

手动的做了主从的切换,当然可以程序来做,这个就是哨兵,不过说哨兵之前,先看看主从复制的配置吧。找到REPLICATION

直接启动的时候追随谁

1
# replicaof <masterip> <masterport>

是否在同步主的数据的时候,自己老的数据是否可查,yes是可以查的,默认是

1
replica-serve-stale-data yes

是否只在从的模式支持查询,默认是

1
replica-read-only yes

主从同步默认,是走网络还是磁盘,其实就是我通过网络发一部分RDB给你立刻同步,还是把RDB文件全部传到磁盘里,从在做同步。

1
repl-diskless-sync no

增量复制,设定具体的值,在同步的时候,可以不用同步全部的数据。

1
# repl-backlog-size 1mb

哨兵

上面手动的进行了主切从,当然人肯定不可能一直做这个,所以需要一个程序,就是哨兵。具体介绍:

Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance), 该系统执行以下三个任务:

  • 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
  • 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。

Redis Sentinel 是一个分布式系统, 你可以在一个架构中运行多个 Sentinel 进程(progress), 这些进程使用流言协议(gossip protocols)来接收关于主服务器是否下线的信息, 并使用投票协议(agreement protocols)来决定是否执行自动故障迁移, 以及选择哪个从服务器作为新的主服务器。

虽然 Redis Sentinel 释出为一个单独的可执行文件 redis-sentinel , 但实际上它只是一个运行在特殊模式下的 Redis 服务器, 你可以在启动一个普通 Redis 服务器时通过给定 –sentinel 选项来启动 Redis Sentinel 。

开始配置:

需要哨兵配置文件,整三个配置文件,启动三个哨兵

1
2
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2

启动redis,并从追随主

1
2
redis-server 6380.conf replicaof 127.0.0.1 6379
redis-server 6381.conf replicaof 127.0.0.1 6379

启动哨兵

1
2
3
redis-server 26379.conf --sentinel
redis-server 26380.conf --sentinel
redis-server 26381.conf --sentinel

当哨兵都启动完成后,查看日志,会发现,不管是redis的主从还是哨兵,都被发现,这个是因为redis有一个订阅的功能,可以使用PSUBSCRIBE *查看消息

1
2
3
4
5
6
7
2841:X 03 Jun 2021 22:53:13.535 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
2841:X 03 Jun 2021 22:53:13.537 # Sentinel ID is 9699837e05d030710b467e1dbfa2245a9a8c6f90
2841:X 03 Jun 2021 22:53:13.537 # +monitor master mymaster 127.0.0.1 6379 quorum 2
2841:X 03 Jun 2021 22:53:13.538 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
2841:X 03 Jun 2021 22:53:13.539 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379
2841:X 03 Jun 2021 22:54:13.903 * +sentinel sentinel 85b99fc000f14a342557c14c90d4a6374304fdde 127.0.0.1 26380 @ mymaster 127.0.0.1 6379
2841:X 03 Jun 2021 22:54:48.271 * +sentinel sentinel 2becd61df1f726da69c313126a8c871f66009079 127.0.0.1 26381 @ mymaster 127.0.0.1 6379

好了,都启动完了,直接把主停掉,过了一会会发现哨兵自动投票选个主,并设置好了,具体看日志,这里就不多说了。

Redis数据的分片(Y、Z)

主从和自动处理故障都说完了,解决了x轴的问题,但是没有解决Y、Z轴的问题,就是容量的问题。

功能分片(Y轴)

按照业务的不同,我可以在客户端按照业务的逻辑,往不同的Redis去写,这个是可分项的,但是如果我的一类的业务数据无法在拆呢???

image-20210605215719868

Hash+取模分片(Z轴)

当数据已经无法在拆分的时候,只能在客户端,对key进行hash值的计算,然后取模我的主Redis实例数量,记住我的用词,是主Redis实例的数量,但是我Redis实例数量是无法被改变的,因为,我要把数据取回来啊,我加个Redis实例,取模的值如果改变了,那么我对应取的Redis实例是不同的,所以Redis的实例数量是一定不能变化的。这种方式没有扩展性。

image-20210605215933923

Random分片

我随机的把Redis数据放到不同的Redis里,但是取的时候无法确定要取那个Redis实例,这个是特殊的应用场景去用的,举个例子:客户端不断的向Redis lpush数据,随机的add到两台redis,另一个客户端对这两台Redis进行rpop,为啥是rpop,因为我lpush是在左边插,去想要按照队列的方式取数据,肯定要从右边取,具体看上面的List操作。

这种方式像是什么?消息队列取消息消费呗,毕竟大数据量的消息怼入一个Redis受不了啊!

image-20210605220421518

hash一致性分片(重点)

做这个非常非常重要,光看流程图都这么长!hash算法是什么?映射算法,如果不想进行hash取模分片,又要可扩展+准确映射Redis实例,所以需要把key和node节点结合起来,做处理,这个怎么去理解,怎么去拿到数据呢?

想象有一个环形,我把node进行hash运算,想象放到了这个环形上,这个是物理节点,因为是实打实对应了node,我有x个node,那么环形上对应了X个物理节点,重点来了。

来了一个key,我想set一个值,我对这个key进行hash运算,对应到了环形的一个节点,我直接在这个环上,找到离最近的物理节点,也就是node节点,把数据写到这个节点!!!然后如何取出数据呢?很简单,在进行hash运算,找到最近的node节点,直接取数据不就完事了吗!!!

这个解决了添加节点问题,但是,每个技术都会存在一些问题。假如说,我添加一个节点,进行hash计算,好巧不巧添加到了一个node节点和key值的中间的节点,当再去key取值的时候,获得的实例是这个新加的节点,造成数据不能命中。

上面的问题,有一个解决方案,可能会解决问题,就是取节点的时候,多取两个节点,假如在没有,那也没有办法了,当然取几个节点,自己去设计,在乎的是人。

还有一个问题,数据倾斜,毕竟key的hash计算无法预知,可能会很多的数据可能存放到一个redis里,那么如何解决?假如我有3个node,我对一个node分别生成10个节点,就完事了。

image-20210605221606614

代理(重中之重)

BB这么多,都是客户端实现的,这边其实已经有人给实现了,这个就是代理,有名的是推特的twemproxy,国人的predxy,只需要配置完启动它,连接代理就可以执行命令了。自动计算映射到不同的Redis实例。

twemproxy

安装教程在github和你详细,直接

1
2
3
4
5
6
7
git clone git@github.com:twitter/twemproxy.git
cd twemproxy
yum install -y automake libtool
autoreconf -fvi
./configure --enable-debug=full
make
src/nutcracker -h

当编译完成,进入src文件夹就看到nutcracker,看到这个就代表编译完成了。可以nutcracker -h查看帮助

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@node01 src]# ./nutcracker -h
This is nutcracker-0.4.1

Usage: nutcracker [-?hVdDt] [-v verbosity level] [-o output file]
[-c conf file] [-s stats port] [-a stats addr]
[-i stats interval] [-p pid file] [-m mbuf size]

Options:
-h, --help : this help
-V, --version : show version and exit
-t, --test-conf : test configuration for syntax errors and exit
-d, --daemonize : run as a daemon
-D, --describe-stats : print stats description and exit
-v, --verbose=N : set logging level (default: 5, min: 0, max: 11)
-o, --output=S : set logging file (default: stderr)
-c, --conf-file=S : set configuration file (default: conf/nutcracker.yml)
-s, --stats-port=N : set stats monitoring port (default: 22222)
-a, --stats-addr=S : set stats monitoring ip (default: 0.0.0.0)
-i, --stats-interval=N : set stats aggregation interval in msec (default: 30000 msec)
-p, --pid-file=S : set pid file (default: off)
-m, --mbuf-size=N : set size of mbuf chunk in bytes (default: 16384 bytes)

不过为了方便使用,需要把twemproxy安装成一个服务级别的程序,后面可以直接systemctl twemproxy start多舒服啊,于是先cp服务级别配置文件,并授权

1
2
3
4
5
[root@node01 scripts]# pwd
/opt/modul/twemproxy-master/scripts
[root@node01 scripts]# cp nutcracker.init /etc/init.d/twemproxy
[root@node01 scripts]# cd /etc/init.d/
[root@node01 init.d]# chmod +x twemproxy

查看配置文件,有两个关键的配置,一个是twemproxy的配置文件,一个是把twemproxy程序的位置

1
2
OPTIONS="-d -c /etc/nutcracker/nutcracker.yml"
prog="nutcracker"

按照他的说明,把twemproxy自带的配置文件cp到/etc/nutcracker/,并把nutcracker移动到/usr/bin

1
2
3
4
5
[root@node01 conf]# pwd
/opt/modul/twemproxy-master/conf
[root@node01 conf]# mkdir /etc/nutcracker
[root@node01 conf]# cp ./* /etc/nutcracker/
[root@node01 conf]# cp ../src/nutcracker /usr/bin/

接下来是要修改nutcracker配置文件,不过修改直接先要备份哦

1
cp nutcracker.yml nutcracker.yml.bak

vi配置,发现有很多配置,毕竟可以代理多个Redis配置实例,这边只需要一个,只需要dG,删除下面的即可。然后,查看配置文件,很好理解,具体百度或查看官方文档。

1
2
3
4
5
6
7
8
9
10
alpha:  ##名字
listen: 127.0.0.1:22121 ##监听端口
hash: fnv1a_64 ##hash算法
distribution: ketama ##数据分布模式
auto_eject_hosts: true
redis: true
server_retry_timeout: 2000
server_failure_limit: 1
servers: ##Redis实例
- 127.0.0.1:6379:1

那就多放两台

1
2
3
servers: ##Redis实例
- 127.0.0.1:6379:1
- 127.0.0.1:6380:1

启动两台redis,在启动nutcracker

1
2
3
4
5
6
7
8
9
cd /opt/redisTest
mkdir data
cd data
mkdir 6379
mkdir 6380
cd 6379
redis-server --port 6379
cd 6380
redis-server --port 6380

想要服务级别启动,报错,不知道为什么,直接手动启动吧

1
nohup /usr/bin/nutcracker -c /etc/nutcracker/nutcracker.yml &

可以直接连接代理

1
redis-cli -p 22121

设置一些字值

1
2
3
4
5
6
7
8
127.0.0.1:22121> set k2 2
OK
127.0.0.1:22121> set k3 4
OK
127.0.0.1:22121> set k1 1
OK
127.0.0.1:22121> set 1 a
OK

单独连接6379、6380redis可以看到数据被分配到不同的Redis里。其他命令就不演示了,注意,聚合操作无法做因为是不同的redis。

predixy

国人的一款Redis代理工具,非常牛皮,具体看介绍,也可以看github的简介,支持中文版,很方便的。8多说了,直接开始配置按照。

因为编译需要c++11比较麻烦,直接拿他准备好的东西就行了。

image-20210607151229255

下载完成解压,解压完毕,看目录非常的清晰,直接配置环境变量+配置就完事了

image-20210607151710280

文档配置说明

predixy的配置类似redis, 具体配置项的含义在配置文件里有详细解释,请参考下列配置文件:

  • predixy.conf,整体配置文件,会引用下面的配置文件
  • cluster.conf,用于Redis Cluster时,配置后端redis信息
  • sentinel.conf,用于Redis Sentinel时,配置后端redis信息
  • auth.conf,访问权限控制配置,可以定义多个验证密码,可每个密码指定读、写、管理权限,以及定义可访问的健空间
  • dc.conf,多数据中心支持,可以定义读写分离规则,读流量权重分配
  • latency.conf, 延迟监控规则定义,可以指定需要监控的命令以及延时时间间隔

提供这么多配置文件实际上是按功能分开了,所有配置都可以写到一个文件里,也可以写到多个文件里然后在主配置文件里引用进来。详细的配置在这里

这边直接用predixy.conf,介绍,我挑选几个我用的

定义名称:

1
Name PredixyUserInfo

定义predixy服务监听的地址,支持ip:port以及unix socket,默认0.0.0.0:7617

1
2
Bind 0.0.0.0:7617
Bind /tmp/predixy

指定工作线程数4,默认1

1
WorkerThreads 4

指定日志文件名

1
Log /var/log/predixy.log

他默认引入try.conf,来声明我要代理的Redis实例,还可以设置cluster、sentinel两钟模式,下面再说,先使用单机版的数据

1
2
3
4
################################### SERVERS ####################################
# Include cluster.conf
# Include sentinel.conf
Include try.conf

修改try.conf,添加实例

1
2
3
4
5
6
ClusterServerPool {
Servers {
+ 127.0.0.1:6379
+ 127.0.0.1:6380
}
}
集群模式启动

启动两台Redis,并启动predixy

1
2
3
redis-server --port 6379
redis-server --port 6380
predixy conf/predixy.conf

直接连接代理,并设置两个值,发现是ok的,然后分别查看两台Redis实例是否被设置值,经过测试是ok的。

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@node01 ~]# redis-cli -p 7617
127.0.0.1:7617> set k1 1
OK
127.0.0.1:7617> set k2 2
OK
127.0.0.1:7617>
[root@node01 ~]# redis-cli -p 6379
127.0.0.1:6379> keys *
1) "k1"
127.0.0.1:6379>
[root@node01 ~]# redis-cli -p 6380
127.0.0.1:6380> keys *
1) "k2"
哨兵模式启动

还可用使用哨兵模式启动,详细介绍请看配置,这边直接设置,具体配置介绍看官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SentinelServerPool {
Databases 16
Hash crc16
HashTag "{}"
Distribution modula
MasterReadPriority 60
StaticSlaveReadPriority 50
DynamicSlaveReadPriority 50
RefreshInterval 1
ServerTimeout 1
ServerFailureLimit 10
ServerRetryTimeout 1
KeepAlive 120
Sentinels {
+ 127.0.0.1:26379
+ 127.0.0.1:26380
+ 127.0.0.1:26381
}
Group ooxx {
}
Group xxoo {
}
}

配置介绍:

Sentinels:哨兵集群服务

Group:对应的是哨兵的名称,会经过映射算法,映射到对应的Redis实例

在去修改predixy.conf文件

1
2
3
# Include cluster.conf
Include sentinel.conf
#Include try.conf

准备三个哨兵,监控两个主从实例。然后在启动两台主从实例,我就不演示了

1
2
3
4
port 26379
sentinel monitor ooxx 127.0.0.1 36379 2
sentinel monitor xxoo 127.0.0.1 46379 2
...

启动代理predixy

1
bin/predixy conf/predixy.conf

启动成功会有日志,并显示主从信息

image-20210608161407160

连接代理执行命令,发现都是ok的

1
2
3
4
5
6
7
8
9
[root@node01 redisTest]# redis-cli -p 7617
127.0.0.1:7617> set k1 1
OK
127.0.0.1:7617> set k2 2
OK
127.0.0.1:7617> get k1
"1"
127.0.0.1:7617> get k2
"2"

还可以使用group名称单独设置到一个主从的redis实例

1
2
3
4
127.0.0.1:7617> set {ooxx}k5 5
OK
127.0.0.1:7617> set {ooxx}k6 5
OK

以上所以命令都要单独连接Redis验证,这里就不演示了。

Redis自带集群

上面都是映射的概念,而Redis自带集群配置,并且有槽位的概念,其实就是预分区,比如说有9个分区,映射成3个mapping,每个mapping对应的是1 2 34 5 67 8 9 ,而一个mapping对应一个Redis,也就说

1 2 3 –>Redis1

4 5 6–>Redis2

7 8 9 –>Redis3

我数据来的时候,还是做hash计算%9,这样就能找到对应的redis,不管是增加还是修改都是ok的。那么如何进行扩展呢?很简单,取2 4取出来,映射到Redis4不就行了,上面已经说了Redis可以把RDB发送到另一个Redis,Redis不断的同步映射的旧数据和增量数据,直到数据一致,这个一瞬间Redis4就可以使用了,也就是说扩展成功,这个时候就想下面映射模型

1 3 –>Redis1

5 6–>Redis2

7 8 9 –>Redis3

2 4 –>Redis4

貌似解决了嘿嘿,但是举例的9个分区肯定是不够用的,直接预分区16384个不就好了!并且Redis已经帮忙做了。这个模式就很牛逼了,我随便扩展,redis自动把对应的分区数据,给添加到我这个新增加的Redis实例里。

并且,他还有个牛逼的地方,持有所有的映射的mapping关系,这个有什么用,假如说,我查找一个key,是A槽位的,但是我使用的B槽位的Redis,他会自动返回一个错误,并告诉你,这个数据在哪个Redis。

实操

去Redis的utils目录查看create-cluster下的工具脚本,Redis已经把脚本准备好了,这么直接拿过来用

1
/opt/modul/redis-6.2.3/utils/create-cluster

查看配置,发现他指定了ip、端口、节点数量、和从数量

1
2
3
4
5
6
BIN_PATH="../../src/"
CLUSTER_HOST=127.0.0.1
PORT=30000
TIMEOUT=2000
NODES=6
REPLICAS=1

直接执行start

1
2
3
4
5
6
7
[root@node01 create-cluster]# ./create-cluster start
Starting 30001
Starting 30002
Starting 30003
Starting 30004
Starting 30005
Starting 30006

他会创建6个节点的实例和配置

image-20210608232902315

直接执行创建,会告诉你我即将启动的node,和他的主备是什么,分的槽位怎么分的

1
./create-cluster create

image-20210608232943314

执行yes执行执行成功

image-20210608233056568

开始连接,并尝试设置值,但是发现不对啊,怎么失败并提示我移动到30003啊

1
2
3
[root@node01 create-cluster]# redis-cli -p 30001
127.0.0.1:30001> set k1 1
(error) MOVED 12706 127.0.0.1:30003

上面已经说了主的Redis实例持有映射关系,连接是30001的redis实例,k1的映射是k3,所以需要去k3设置值,但是这样太麻烦,于是可以集群的模式启动。再去设置k1,自动移动到30003,并设置值成功。他有自己移动的功能。

1
2
3
4
5
6
7
8
9
10
11
12
[root@node01 create-cluster]# redis-cli -c -p 30001
127.0.0.1:30001> set k1 1
-> Redirected to slot [12706] located at 127.0.0.1:30003
OK
127.0.0.1:30003> set k2 2
-> Redirected to slot [449] located at 127.0.0.1:30001
OK
127.0.0.1:30001> set k3 3
OK
127.0.0.1:30001> set k4 4
-> Redirected to slot [8455] located at 127.0.0.1:30002
OK

在测试下事物,我想监控k1,开启事物,执行完毕在提交命令。发现并不行,因为执行总是根据命令跳来跳去的,我开启的是30003的事物,不管30002的事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:30002> WATCH k1
-> Redirected to slot [12706] located at 127.0.0.1:30003
OK
127.0.0.1:30003> MULTI
OK
127.0.0.1:30003(TX)> set k1 111
QUEUED
127.0.0.1:30003(TX)> set k2 222
-> Redirected to slot [449] located at 127.0.0.1:30001
OK
127.0.0.1:30001> set k4 4
-> Redirected to slot [8455] located at 127.0.0.1:30002
OK
127.0.0.1:30002> EXEC
(error) ERR EXEC without MULTI

于是需要加上标记,发现,只要我加上标志,并不会自动跳转到其他Redis

1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:30003> WATCH {xx}k1
OK
127.0.0.1:30003> MULTI
OK
127.0.0.1:30003(TX)> set {xx}k5 5
QUEUED
127.0.0.1:30003(TX)> get {xx}k5
QUEUED
127.0.0.1:30003(TX)> EXEC
1) OK
2) "5"

上面讲完了,还有其他的命令,譬如分槽位,等。先停止集群并清空。

1
2
./create-cluster stop
./create-cluster clean

使用自己的Redis,创建集群,开启6个redis实例6390-6395,这里就不做演示了。然后执行脚本创建集群,指定副本数是1

1
redis-cli --cluster create 127.0.0.1:6390 127.0.0.1:6391  127.0.0.1:6392  127.0.0.1:6393  127.0.0.1:6394  127.0.0.1:6395 --cluster-replicas 1

这里有个坑,需要给配置文件属性cluster-enabled改成yes。可以查看帮助

1
redis-cli --cluster help

创建节点

1
2
create         host1:port1 ... hostN:portN
--cluster-replicas <arg>

校验节点

1
2
check          host:port
--cluster-search-multiple-owners

查看节点信息

1
info           host:port

分槽位

1
2
3
4
5
6
7
8
9
             --cluster-fix-with-unreachable-masters
reshard host:port
--cluster-from <arg>
--cluster-to <arg>
--cluster-slots <arg>
--cluster-yes
--cluster-timeout <arg>
--cluster-pipeline <arg>
--cluster-replace

添加节点

1
2
3
add-node       new_host:new_port existing_host:existing_port
--cluster-slave
--cluster-master-id <arg>

删除节点

1
del-node       host:port node_id

时间原因就不展示了,很好理解

Redis实际开发

BB了这么多概念,怎么用他啊,怎么实操啊,使用过程中会遇到什么问题啊?直接搞起~

缓存特有问题

缓存击穿

当一个或者少量的key,被大量的访问,但是缓存里没有,需要去数据库拿到数据,这个时候就是缓存击穿。抓住核心词,大量、缓存没有、访问数据库,其实引用缓存就是为了分担数据库的压力,但是分担压力也要看你怎么去设计。

解决思路:

假设有一万笔请求访问一个不存在的key,正常的逻辑缓存没有,就去数据库拿啊,数据库瞬间接收一万请求,导致数据库击穿,这个肯定不合适!

那可以这样做,加锁,第一个请求进来的时候,判断为空,马上nx加上锁,另外的请求sleep一段时间,第一个请求,查出数据,设置进redis里面,剩下的请求sheep完毕,就可以从redis里面拿了。

这里需要注意的点是sheep的时间,和查询数据库的时间,需要做个策略,为啥呢。因为,剩下的请求依赖于第一个请求查询数据库的时间,加上设置redis成功的时间,seelp完毕之后肯定要马上去Redis拿。

所以,去数据库请求的这个线程,需要在新建一个守护线程,判断是否查询去数据库查询的操作,是否完成,如果没有,那么在增加点sheep时间,如果,查询不到数据,那么直接返回null。

缓存穿透

客户端不断的请求不存在的数据,导致一直要查询数据库,从而使数据库的压力过大。上面已经说过,使用布隆过滤器来解决这个问题,三种方式解决。

  • 客户端布隆计算,存储到本地
  • 客户端布隆计算,存储到redis
  • 客户端调用api,由redis计算+存储
缓存雪崩

雪崩的概念和缓存穿透有点类似,都是大量的请求进来访问,缓存又过期了,导致大量的流量打到了数据库。解决方法很简单:合理的分配过期时间。雪崩当然不止这一个概念,举个例子:有一批缓存数据需要在每天的凌晨12点更新,这个还是缓存击穿问题,具体解决办法参考缓存击穿。

SpringBoot实操

经常用的,就不多说了,下面直接进入zookeeper吧。