redisredis架构&系统设计

Redis持久化、Redis删除策略、Redis淘汰策略、Red

2021-06-11  本文已影响0人  想回家种地的程序员

基于c语言开发高性能key-value存储非关系形数据库数据库。

一 基础知识

1.1 五种类型操作

1.1.1 String

1. 脚本操作:

# 添加
set key value
# 获取
get key
# 删除数据
del key
# 添加或者修改多个数据
mset key1 value1 key2 value2
# 获取多个数据
mget key1 key2

1.1.2 hash

每一个key对应的value,类似HashMap集合存储数据的结构。底层使用哈希表结构实现数据存储。

在这里插入图片描述
1. 脚本操作:
# 添加或者修改数据
hset key field value
# 获取数据
hget key fileld
hgetall key
# 删除数据
hdel key field1 [field2]
# 添加或者修改数据
hmset key field1 value1 field2 value2
#获取多个数据
hmget key field1 field2 

1.1.3 List

存储一个key对应多个数据,并且数据的存入和取出顺序是一致的。并且底层使用双向列表。
1. 脚本操作:

# 添加/修改数据
lpush key value1 [value2] ......
rpush key value1 [value2] ......
# 获取数据
lrange key start stop
lindex key index
# 获取并移除数据
lpop key
rpop key

1.1.4 Set

一个key对应多个value值,不能存储重复元素。存储大量数据,查询效率更高。
1. 脚本操作:

#  添加数据
sadd key value1 [value2]
# 获取全部数据
smembers key
# 删除数据
srem key member1 [member2]

1.2 key

1.2.1 key基本操作

key是一个字符串,在redis中通过key能获取值。
1. 脚本操作:

# 删除指定key
del key
# 获取key是否存在
exists key
# 获取key类型
type key
# 设置key有效时间
expire key seconds
pexpire key milliseconds
# 获取key有效时间
ttl key
pttl key

二 持久化

reids中数据存储在内存中。如果redis一直运行,则数据会一直保存在内存中,我们随时都可以读取。但是在现实中,redis可能可能死机,或者部署redis服务器崩溃了。需要重启服务器或者redis,redis原先内存中数据就会丢。所以为了保证redis中数据的安全性,redis就设计了持久化。redis存储数据时候,会同时将数据存储到硬盘上。如果重启redis,将硬盘数据恢复到redis中。Redis持久化到硬盘中两种方式,RDB(日志指令)和AOF(数据快照)。

2.1 持久化基本概述

1. 什么是持久化:
将内存种数据存储到内盘中等永久存储,在一定的时机再从新恢复数据。

2. 持久化两种方式:
计算机中数据是二进制存储,将这二进制数据原封不动的记录下来,也叫快照存储(RDB),保存的是某一时刻数据。
将改变数据的操作命令保存下来,即保存操作过程,称为日志(AOF)。

2.2 RDB(快照)

2.2.1 save指令

1. 手动执行save指令:

save

2. save指令相关配置:

#配置本地数据库文件名,默认dump.rdb,常设置为dump-端口号.rdb
dbfilename filename
# 设置存储文件名
dir path
# 存储到本地数据库,是否压缩
rdbcompression yes|no
# 在读写过程是否对RDB格式校验,节约10%的时间消耗
rdbchecksum yes|no

备注:

save指令会阻塞当前Redis服务器,知道RDB过程完成位置,会造成长时间阻塞。不建议使用

2.2.2 bgsave

1. 手动执行bgsave指令:

bgsave

2. bgsave指令相关配置:

# 后台出现错误,是否停止保存,默认yes
stop-writes-on-bgsave-error yes|no

dbfilename filename  
dir path  
rdbcompression yes|no  
rdbchecksum yes|no

3. 配置bgsave自动执行:

监控时间key变化量

save second changes
例如:
# 900秒,有一个key发生变化
save 900 1
# 300秒,有10个key发生变化
save 300 10
# 60秒,有10000个key发生变化
save 60 10000

完整配置

save second changes
filename filename
dir path
rdbcompression yes|no
rdbchecksum yes|no
stop-writes-on-bgsave-error yes|no

4. bgsave执行原理:

在这里插入图片描述
针对save命令进行优化,Redis中所有涉及RDB操作都采用bgsava方式。
当客户端给服务端发送一个save指令时候,服务端立马给客户端返回一个结果,同时创建一个子线程执行save操作。

2.2.3 RDB总结

优点:
RDB时一个二进制文件,存储效率很高,比AOF速度快很多。

缺点:
无法做到实时持久化,容易造成数据丢失。
因为bgsave都会创建一个子线程,会牺牲一些性能。
Redis众多版本中,RDB的二进制文件无法统一,各个版本的服务之间数据格式无法兼容。

2.3 AOF(日志)

以独立日志方式,记录每次读写命令。重启服务时,执行AOF文件中命令。主要用于解决数据实时性,是Redis持久化的主流方式。

2.3.1 AOF日志配置

# 开启AOF持久化功能
appendonly yes|no
# AOF持久化名字,默认名字为appendonly.aof
appendfilename filename
# AOF保存路径,和RDB路径保持一致即可
dir path
# AOF写数据策略,默认everysec
appendfsync always|everysec|no

备注(AOF写三种策略):
always(每次):每次写操作都会同步到AOF文件中,性能低。
everysec(每秒):每秒将缓冲区命令同步到AOF文件中,系统在突然宕机情况会有一秒钟数据丢失,数据准确性较高,性能高,建议使用。
no(系统控制):操作系统控制每次同步到AOF文件周期。

2.3.2 AOF重写

对同一数据若干指令转化为最终结果的对应指令。
1. 重写的作用:
将多条命令压缩成一条。降低磁盘占用率,提高恢复效率。

2. 重写的规则:
进程中有时效数据,并且已经超时,不写进AOF文件中。
对于无效指令,直接忽略。
对一条数据的多条写命令合并成一条命令。

3. 重写配置:
手动执行

bgrewriteaof
在这里插入图片描述

自动重写配置

auto-aof-rewrite-min-size size
auto-aof-rewrite-percentage percentage

4. AOF写和重写流程:

在这里插入图片描述

2.4 RDB和AOF应用场景

1. 对数据十分敏感,建议使用AOF持久化方案:

AOF持久化策略使用everysecond,默认一秒钟同步一次。该策略redis仍然能保持很好的性能,
当机器突然出现故障,也就丢失0-1秒钟数据。

2. 数据呈现阶段有效,使用RDB持久方案:
良好的保持数据阶段不丢失,且恢复时间快。

三 数据删除与淘汰策略

3.1 过期数据删除(key已经过期)

3.1.1 数据状态

内存中数据通过TTL指令获取状态

正数: 数据在内存中有存活时间。
-1:永久存在。
2:已过期数据,或者被删除的数据。

3.1.2 Redis中时效性数据存储结构

在这里插入图片描述

过期数据是一块独立的存储空间,Hash结构。field是value的内存地址,value是过期时间。最终进行过期处理时候,对该空间数据进行检测,当时间到期后通过filel找到数据地址,进行相关操作。

3.1.3 数据删除策略(过期数据)

在内存占用和cpu占用之间寻找一种平衡,不能顾此失彼造成redis性能下降,引发服务器宕机和内存泄漏。针对过期数据删除策略如下:

定时删除
惰性删除
定期删除

1. 定时删除:
创建一个定时器,key设置过期时间,当到达过期时候,定时器任务立即执行对键删除。
总结:
到时就删除,节约内存。但是造成cpu压力大,影响服务器响应时间和指令吞吐量。即拿时间换空间。

2. 惰性删除:
数据到达过期时间,不做处理。下次访问该数据时进行删除。

内存压力大,长期占用内存空间。但是节省CPU性能,发现时删除。即拿时间换空间。

3. 定期删除:
相对前两种方案的一种折中方案。
删除过程:

Redis启动服务器初始化时,读取配置server.hz的值,默认为10.
每秒执行server.hz次serverCron()-------->databasesCron()--------->activeExpireCycle()
activeExpireCycle()对每个expires[]进行逐一监测。
对某个expires[
]检测时,随机挑选W个key检测

如果key超时,删除key。
如果一轮中删除的key的数量>W*25%,循环该过程。
如果一轮删除的key的数量<W*25%,检查下一个expires[*]
W取值=ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP属性值

总结:
周期性轮询redis库中时效性数据,采用随机抽取数据,利用过期的比例来控制删除频率。

检测频率可以自定义设置,内存压力不是很大,长期占用内存的冷数据会被持续的删除。即随机抽查重点抽查。

3.2 数据淘汰(redis中内存不足)

在redis中执行命令之前会调用freeMemoryIfNeeded()检查内存是否充足。如内存不满足,则要删除一些数据。清除数据策略称为逐出算法。

3.2.1 策略配置

# 使用最大内存,生成环境上设置为50%以上
maxmemory ?mb
# 每次选取待删除数据个数
maxmemory-samples count
# 对数据进行删除,选择策略
maxmemory-policy policy

删除数据策略(三类八种)
第一类:检测易失数据

volatile-lru:挑选最近最少使用的数据淘汰
volatile-lfu:挑选最近使用次数最少的数据淘汰
volatile-ttl:挑选将要过期的数据淘汰
volatile-random:任意选择数据淘汰

第二类:检测全库数据

allkeys-lru:挑选最近最少使用的数据淘汰
allkeLyRs-lfu::挑选最近使用次数最少的数据淘汰
allkeys-random:任意选择数据淘汰,相当于随机

第三类:放弃数据驱逐

no-enviction(驱逐):禁止驱逐数据(redis4.0中默认策略),会引发OOM(Out Of Memory)

三 原理分析

3.1 基础概念

1. Redis是单线程还是多线程架构:
redis整体并非是一个线程,redis在处理网络请求和k/v读写操作时候是一个线程。而持久化,异步删除,集群数据同步都是额外线程进行处理。

2. 单线程为啥这么快?:
redis大部分操作是基于内存中。
因为只有一个线程,避免多线程上下文切换和竞争。
redis底层采用IO多路复用技术,大量高并发下,提高系统吞吐量。

3.2 多路复用

3.2.1 前置知识

1. file descriptor(fd):
文件描述符。(Linux中一些皆文件,比如:普通文件、目录文件、连接文件、设备文件等。)文件描述符是内核为了高效管理系统打开文件而产生的一个索引(指针),是一个非负数,所有的io操作的系统调用都是通过fd来操作。
2. 内核空间和用户空间:

在这里插入图片描述
内核能管理系统所有资源,磁盘读写,网络IO读写,内存分配回收、进程管理等;他能访问受保护资源,能访问底层硬件设备。应用程序想要操作底层硬件必须通过内核来进行访问。

3.2.2 epoll(IO多路复用分析)

3.2.2.1 阻塞型通信分析

在这里插入图片描述
阻塞网络IO流程图:
在这里插入图片描述
a 启动TestSocket服务->代码执行流程如下:
b ServerSocket ss = new ServerSocket(8888);--->
    内核调用socket()方法,返回一个文件描述符3。
    调用bind(3,8888)方法,将fd为3绑定8888端口。
    调用listen(3,50),创建socket监听对象,fd为3监听8888端口。
c Socket s = ss.accept();
    调用系统函数accept(3,...) = 5,服务端应用等待着内核响应,内核等待着客户端连接,所以应用处理阻塞中。
    当有客户端连接创建一个socket连接对象。
d 获取客户端发过来数据。
    调用系统函数recv(5,...) recvfrom(5,...) recvmsg(5,...)函数,内核等到客户端的数据发送。应用等待内核响应。
    内核接受到数据,返回给应用。
e 将数据返回到客户端。
    应用调用send(5,...)等系统函数,将数据发给内核。内核发送完毕返回给应用。

总结:
在服务端接受客户端数据时候,因为应用通过内核调用系统函数accept(3,...) 、recv(5,...)、send(5,...),必须等到内核响应后,应用才能进行下一步操作。所以等待连接、等待发送数据、发送数据都是阻塞的。
因为应用是一个主线程执行,所以当有多个客户端进行连接。必须等待上一个线程完全执行完主线程,才能进行下一个线程处理。

3.2.2.2 非阻塞网络传输分析

在这里插入图片描述
异步通信和上面同步通信流程类似。只不过在调用系统函数accept(3,...) 、recv(5,...)、send(5,...)等前,都调用系统函数fcntl(5,....)
导致应用调用accept(3,...) 、recv(5,...)、send(5,...)立马执行下一步,不用等待内核处理完成响应数据。从而导致程序异步。提高
网络通信的性能。
但是因为应用不等待内核处理完毕,所以应用要不断的轮询向内核获取数据。

3.2.2.3 一个线程处理多个客户端连接

linux中提供了select/poll/epoll系统函数支持这一模型。它支持应用程序将一个或者多个fd交给内核,内核检测fd上的状态变化(连接、读、写等)


在这里插入图片描述

流程分析


在这里插入图片描述
a 应用程序调用epoll_create(1024)函数返回fd为5。
    创建epoll对象,创建监听树,创建队列
    socket函数,创建一个fd6
    bind函数,将fd6进行绑定
    listen将fd6监听
    fcntl函数让epoll_ctl变成异步。
b 调用epoll_ctl函数
    创建一个socket节点到监听树上(注册了节点的监听事件)。
c epoll_wait 函数
    轮询的从队列中取出,然后处理多个事件。保证了一个线程处理多个客户端连接。

总结:
epoll模型优势:
没有fd模型限制,不会随着fd增加导致IO效率降低。
不需要每次将fd拷贝到内核空间,只需要一次拷贝,后面复用。
mmap技术加速了用户空间和内核空间数据访问。

3.2.3 Select(IO多路复用)

在这里插入图片描述
总结:
select支持跨平台的。但是需要自己维护fd_set,每次都需要fdSet从用户空间拷贝到内核空间。如果fd较多则是一个耗时操作。如果fdset中只有少数的fd在活跃,性能不高。

3.2.4 高性能的Redis有哪些慢操作?什么样操作影响性能? 在这里插入图片描述

四 Redis高可用之-主从复制

当redis服务不可用时候,导致应用服务不可用。物理故障可能导致数据丢失。redis提供了主从复制功能。

4.1 Redis主从复制初认识

4.1.1 主从复制优点和弊端

1. 优点:
如果主节点宕机后,从节点作为主节点备份顶上来。扩展了主节点读能力。

2. 缺点:
a master宕机之后,需要从slave选出一个master。然后其他的slave需要复制新master数据。同时还需要通知客户端新的master数据。这个过程就叫做故障转移,但是限制这个过程仍然需要人工参与。
b redis的写能力依然受到单机的限制。
c redis存储能力也受单机的限制。

4.1.2 主从复制流程

1. 建立连接:

在这里插入图片描述
2. 数据同步:
在这里插入图片描述
数据同步注意问题:
a master数据量大,数据同步应该避免高峰。
b 复制缓冲区大小要合理,会导致数据溢出。或者复制周期长,复制部分数据,发现数据丢失,进行第二次全量复制,slave陷入死循环。
c master主机内存不要太大,占用百分之50-70。留百分之30-50用于执行bgsave和创建复制缓冲区。

3. 命令转移:
master库数据状态被修改后,导致主从服务器状态不一致,需要让主从同步到一致的状态。同步叫命令传播。

复制缓冲器:
是一个队列,用于存储服务器执行命令。每次传播命令,master都会将命令记录下来,并存储到复制缓冲区中。

总结:
[站外图片上传中...(image-ac6d25-1623382960682)]

4.2 哨兵架构

redis节点出现故障时,sentinel能够自动完成故障发现和转移,并通知应用端,实现真正高可用。

4.2.1 哨兵架构分析

在这里插入图片描述
1. 哨兵架构做了那些事情:
监控: 每个哨兵节点会定时检测redis数据节点以及其他sentinel节点是否可达。

主节点故障转移: 当master出现故障时候,从slave节点中选取一个master,从slave复制新的master的数据。并且维持后续主从关系。

通知: 哨兵在完成故障转移后,会通知连接客户端故障转移的结果。

配置提供者: 哨兵架构中应用端配置了sentinel的节点集合,通过sentinel获取master信息。

2. sentinel配置多个节点好处?
对主节点进行故障判断是由所有的sentinel共同判断,防止误判。
sentinel有多个,即使个别的sentinel不可用,但整体还是可以用的。

4.2.2 sentinel三个定时任务

1. 第一个定时任务:

在这里插入图片描述

每隔10秒,每个sentinel节点向master和slave发送info命令,作用如下:

a 向主节点发送info命令可用获取从节点信息,(sentinel无需配置从节点),当有新的从节点加入,立刻感应维护正确的拓扑结构。
b 根据info命令的回复动态更新sentinel中维护主从节点的完整信息。

2. 第二个定时任务:
[站外图片上传中...(image-3be466-1623382960682)]

每隔2s,每个sentinel节点向sentinel:hello节点发布当前sentinel信息和sentinel对主节点判断,同时其他sentinel节点订阅该频道,作用如下:

a 发现其他sentinel节点。
b sentinel节点交换主节点状态,作为客观下线和领导者选举的依据。

3. 第三个定时任务:
[站外图片上传中...(image-7ae324-1623382960682)]
每隔一秒钟每个sentinel要向其他sentinel节点和redis节点发送ping。判断这些节点是否可达,从而检查节点健康状况。

4.2.3 主观下线和客观下线

[站外图片上传中...(image-39944e-1623382960682)]
主观下线是某一个sentinel一家之言,存在误判操作。
如果是从节点或者其他sentinel节点主观下线,没有后续操作。如果是主节点还需进行客观下线判断。

4.2.4 故障转移过程

  1. 没有足够的sentinel节点同意主节点下线,主节点主观下线被移除。当主节点从新向sentinel发送ping有效回应时,主节点主观下线移除。
  2. 主节点下线后,sentinel向主节点和所有从节点发送info命令,由之前10s一次变为每秒一次。
  3. 接下来进行故障转移前选举出sentinel的leader
    因为故障转移工作仅仅是需要一个sentinel节点完成。所有sentinel节点之间要进行选举,选出一个leader完成故障转移。
        每一个sentinel都有可能成为leader。   
        每个sentinel只有一张票,只能投给sentinel,先到先得。
        如果某个sentinel得票数>=一半,选举成功。如果选举失败,进行下一轮选择。

[站外图片上传中...(image-e38a21-1623382960682)]

  1. sentinel的leader节点完成故障转移
    从salve节点中选举一个从节点,并升级为主节点。
    从节点从新复制主节点数据。
    已下线主节点变成从节点,并复制新的主节点数据。(这个设置由于主节点已经下线,无法立刻通知。只能将该设置放到sentinel中,当下线主节点上线,sentinel会将设置发送)
    将故障转移结果告诉其他sentinel。(sentinel-leader节点将主节点相关信息,通过发布订阅方式完成)

[站外图片上传中...(image-721261-1623382960682)]

  1. sentinel将故障转移结果通知客户端。
    [站外图片上传中...(image-62f77d-1623382960682)]
    sentinel会在相关频道发布故障转移相关信息,应用端只需要去自己感兴趣频道订阅即可。
a 根据sentinel节点 调用sentinelGetMasteAddrByName获取master相关信息。
b 为每一个sentinel创建一个监听线程,并订阅“+switch-master”的频道。(sentinel完成故障转移后会在“+switch-master”频道发布新的master信息)
c 客户端从新初始化,连接master。

五 Redis高可用之-集群

虽然主从复制和哨兵架构能解决高可用问题。但是无法扩展写能力和存储能力。大数据量和高并发无法满足高可用。redis提供了分布式解决方案。

5.1 分布式解决方案

1. 客户端分区:
[站外图片上传中...(image-468e5a-1623382960682)]
2. 代理分区:
[站外图片上传中...(image-70cb1c-1623382960682)]
3. 查询路由分区:
[站外图片上传中...(image-dbafd7-1623382960682)]

5.2 redis分区理论

cluster采用虚拟槽分区方案。redis定义了16384个槽,编号0-16383。每个master属于16384哈希槽中一部分。
执行GET/SET/DEL根据key进行操作,Redis通过CRC16算法计算key,得到redis节点。然后操作指定的redis节点。
[站外图片上传中...(image-f83d7a-1623382960682)]
redis扩容:
新增的redis节点中没有卡槽分配,因此需要从新分配卡槽,还需要考虑redis中数据迁移。

配置文件:

./redis-cli --cluster reshard 192.168.211.141:7001 --cluster-from c9687b2ebec8b99ee14fcbb885b5c3439c58827f,80a69bb8af3737bce2913b2952b4456430a89eb3,612e4af8ea e48426938ce65d12a7d7376b0b37e3 --cluster-to 443096af2ff8c1e89f1160faed4f6a02235822a7 -cluster-slots 100 

#参数说明
--cluster-from:表示slot目前所在的节点的node ID,多个ID用逗号分隔 
--cluster-to:表示需要新分配节点的node ID --cluster-slots:分配的slot数量 

1. Cluster请求路由:
[站外图片上传中...(image-d2345f-1623382960682)]

六 灾难解决

6.1 缓存穿透

用户查询缓存没有查到数据,然后查询MySQL数据库也没有查询到数据,然后用户反复刷新导致反复查询数据库,这种现象叫做缓存穿透。
1. 解决方案一:
第一次查询查询缓存和数据库都没查到数据,此时将null做为value存储到缓存中。下次同样的请求就直接从缓存中取出null。
[站外图片上传中...(image-cf4221-1623382960682)]
2. 第二种解决方案(布隆过滤器):
布隆过滤器是什么?:

用于解决大规模数据情况下不需要精准过滤场景。
布隆过滤器内部是一个bit数组,以及2个hash函数((f_1,f_2))。布隆过滤器有个误判率概念,误判率越高,数组越短,误判率越低,数组越长。
如果有两个数字,N_1经过函数f_1,f_2计算出两个数字,让存储到bit数组中。N_2经过f_1 f_2计算也产生两个数字。当两个数字和和N_1产生的数字有一个一样,则代表N_2在集合中,这就是布隆过滤器的计算原理。

[站外图片上传中...(image-1c22ae-1623382960682)]
解决思路:
在查询缓存时候,先去缓存布隆器中查询。如果在缓存布隆器中查询到结果后,则进行缓存查询。如果没有查询到直接返回,不进行查询。

6.2 缓存击穿

缓存过期,正在此时大量的请求访问某个key,大量请求查询数据库,这种现象叫做缓存击穿。
定时器:
后台定义一个定时器,定时主动更新缓存。例如:某个在缓存中的数据,在一分钟过期,我每隔30秒去更新下缓存数据。
这种方案思路简单,但是增加系统的复杂性。对于key相对固定的,适合。
多级缓存:
[站外图片上传中...(image-6a1ae3-1623382960683)]
我们应用程序将数据存储到缓存中,并设置永不过期。用户进行查询的时候,先查询ngnix缓存,缓存不存在,查询redis缓存中,并将数据存储到Nginx一级缓存,并设置更新时间。不仅防止缓存击穿,还提升程序抗压能力。

分布式锁(解决超卖):

和锁、同步代码块实现功能是一样的,只是使用的业务场景不一样。普通锁和同步代码块只能解决单体服务的,分布式锁解决分布式集群环境。

使用Redission实现分布式锁:

1. 引入依赖包。
2. 创建redis集群配置文件,添加配置。
3. 定义获取锁、释放锁方法。
4. 创建Redisson工厂。

[站外图片上传中...(image-7174ec-1623382960683)]

队列术:

面对封流时候,可以直接将流量放到队列中。让后台不用同时处理更多请求,让队列中请求逐个消费。

Nginx缓存队列术:

了Nginx的代 理缓存,其中有一个属性叫 proxy_cache_lock。多个客户端请求一个缓存中不存在的文件。只允许第一个请求发送到服务端,其他请求在缓存中取到取到信息。

6.3 雪崩

大量的缓存失效,导致大量请求查询数据库,这种现象叫做雪崩。
解决方案:

多级缓存
限流
队列限流
数据预热

6.4 缓存一致性

数据库中数据发生了更改,需要更新缓存中的数据。解决方案canal。

6.4.1 缓存一致性的原理

[站外图片上传中...(image-ecd675-1623382960683)]

使用Canal监听数据库指定表的增量变化,在Java程序中消费Canal监听到的增量变化,并在Java程序中实现对Redis和Nginx缓存更新。

1. Mysql主从复制原理:

a 将mysql master数据变更记录到二进制文件中。 
b MySQL slave将master二进制文件拷贝到自己中继日志中。
c MySQL slave从放relay log日志,同步变更数据。

2. Canal工作原理:

a Canal伪装成slave,向master发送drump协议。
b master接受命令之后,向slave(Canal)发送binary log。 
c Canal开始解析binary log。

6.4.2 认识Canal

Canal用于基于Mysql增量日志解析,并提供增量数据订阅和消费。

1. Canal应用场景:

搜索引擎和缓存的更新。
代替轮询方式来对数据库表变更进行监控,有效缓解轮询导致数据库资源。

6.4.3 Canal的配置

1. 开启MySQL的bin-log日志:

cd /etc/mysql/mysql.conf.d 
 
在mysqld.cnf下面添加如下配置
 # 开启 
binlog log-bin=/var/lib/mysql/mysql-bin 
# 选择 ROW 模式 
binlog-format=ROW 
# 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复 
server-id=12345

2. Canal安装:

docker run -p 11111:11111 --name canal -d docker.io/canal/canal-server

3. 配置CanalServer:
配置Canal的id:

 /home/admin/canal-server/conf/canal.properties 

[站外图片上传中...(image-906a36-1623382960683)]
配置数据库监听地址和监听数据库以及表变化:

/home/admin/canal-server/conf/example/instance.properties

[站外图片上传中...(image-16f8ce-1623382960683)]

配置regex规则
a 多个正则用逗号隔开(,),转义符要双斜杠(\\)
b 所有表:.*   or  .*\\..* 
c  canal schema下所有表: canal\\..* 
d  canal下的以canal打头的表:canal\\.canal.*
e  canal schema下的一张表:canal.test1 
f  多个规则组合使用:canal\\..*,mysql.test1,mysql.test2 (逗号分隔) 

重启canal:

docker restart canal

MySQL创建账号并授权:

create user canal@'%' IDENTIFIED by 'canal'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES; 

6.4.4 同步更新缓存

1. 创建类MoneyLogSync,继承EntryHandler类,实现方法:

@CanalTable(value = "money_log")
    @Component
    public class MoneyLogSync implements EntryHandler<MoneyLog> {

        @Autowired
        private MoneyLogService moneyLogService;

        @Autowired
        private RedisTemplate redisTemplate;

        /**
         * 数据增加变更
         * @param moneyLog
         */
        @Override
        public void insert(MoneyLog moneyLog) {
            //查询用户的抢红包列表         
            List<MoneyLog> moneyLogs = moneyLogService.list(moneyLog.getUsername());
            //将数据存入到Redis         
            redisTemplate.boundHashOps("UserMoneyLog").put(moneyLog.getUsername(), moneyLogs);
            //更新nginx一级缓存同样道理
        }
       
    }

配置Canal地址:

#Canal配置 
canal:   
    server: 192.168.211.141:11111   
    destination: example

七 RESTful站点安全终极解决方案

lua脚本解决基于RESTFul开发安全风控解决方案:【缓存穿透】、【缓存击穿】、【缓存雪崩】、【黑白名单】、 【定向日志收集】、【防止攻击】、【限流】、【熔断】
[站外图片上传中...(image-f07850-1623382960683)]

ngnix和lua脚本结合,用lua脚本对请求进行分析,然后降请求转发下去。

[站外图片上传中...(image-f6762e-1623382960683)]
1. lua执行http请求:

--输出json类型数据
        ngx.header.content_type="application/json;charset=utf8"
        local http = require "resty.http"

        local httpc = http.new()
        --执行请求
        local res, err = httpc:request_uri("http://192.168.0.105:18082/api/userinfo/one", {  
            method = "GET",  
            body = "a=1&b=2",  
            headers = {   
                    ["Content-Type"] = "application/x-www-form-urlencoded", 
            },
            --当前连接保存时间
            keepalive_timeout = 60000,
            --连接池数量
            keepalive_pool = 10
        })
        if not res then  ngx.say("failed to request: ", err)  return end
        ngx.say(res.body)

修改nginx.config

#api
location ~ /api {
     content_by_lua_file /usr/local/openresty/nginx/lua/resthttp.lua; 
 }

八 Nginx缓存学习

Nginx处于Web网站服务最外层,而且支持浏览器缓存配置和后端缓存,用它做部分数据缓存效率更高。

8.1 实现浏览器缓存

8.2 Nginx清理缓存

如果不想采用缓存过期,采用第三方缓存清理模块,nginx_ngx_cache_purge 。在安装OpenRestry就已经实现了。
1. 配置清理缓存:

#清理缓存 
location ~ /purge(/.*) {    
    #清理缓存    
    proxy_cache_purge openresty_cache $host$1$is_args$args; 
}

2. 查看缓存:
每次请求 hostkey 就可以删除指定缓存,我们可以先查看缓存文件的可以
[站外图片上传中...(image-c942ad-1623382960683)]
访问路径:http://ip+port/purge/user/wangwu

8.3 Lua脚本基本语法

百度吧

8.4 多级缓存架构

1. 用Java实现多级缓存:
[站外图片上传中...(image-86551f-1623382960683)]

1、用户请求经过Nginx 
2、Nginx检查是否有缓存,如果Nginx有缓存,直接响应用户数据 
3、Nginx如果没有缓存,则将请求路由给后端Java服务 
4、Java服务查询Redis缓存,如果有数据,则将数据直接响应给Nginx,并将数据存入缓存,Nginx将数据响应给用户 
5、如果Redis没有缓存,则使用Java程序查询MySQL,并将数据存入到Reids,再将数据存入到Nginx中

2. 用lua脚本实现多级缓存:
[站外图片上传中...(image-5d44f4-1623382960683)]

nginx+lua多级缓存架构搭建,用lua脚本实现连接redis和mysql。避免了tomcat并发能力瓶颈。
用户请求查询数据:
    a 先查询nginx缓存,如果缓存存在直接响应,不存在直接用lua脚本查询redis。
    b redis中有数据直接响应,并把缓存加载到nginx中。如果没有查询到缓存,查询MySQL。
    c 查询到数据,响应用户,然后以次放入到redis和nginx缓存中。

Lua脚本连接MySQL:

--MySQL查询操作,封装成一个模块
--Java操作MySqL
--导入依赖包
local mysql = require "resty.mysql"

--配置数据源链接
local props = {
    host = "192.168.211.141",
    port = 3306,
    database = "redpackage",
    user = "root",
    password = "123456"
}

--创建一个对象
local mysqldb = {}


--查询数据库
function mysqldb.query(sql)
    --创建链接
    local db = mysql:new()
    --设置超时时间
    db:set_timeout(10000)
    db:connect(props)

    --配置编码格式
    db:query("SET NAMES utf8")

    --查询数据库 "select * from activity_info where id=1"
    local result = db:query(sql)
    
    --关闭链接
    db:close()
    --返回结果集
    return result
end

return mysqldb

Lua脚本连接Redis:

--操作Redis集群,封装成一个模块
--引入依赖库
local redis_cluster = require "resty.rediscluster"

--配置Redis集群链接信息
local config = {
    name = "test",
    serv_list = {
        {ip="192.168.211.141", port = 7001},
        {ip="192.168.211.141", port = 7002},
        {ip="192.168.211.141", port = 7003},
        {ip="192.168.211.141", port = 7004},
        {ip="192.168.211.141", port = 7005},
        {ip="192.168.211.141", port = 7006},
    },
    idle_timeout    = 1000,
    pool_size       = 10000,
}

--定义一个对象
local lredis = {}

--创建set()添加数据方法
function lredis.set(key,value)
    --1)打开链接
    local red = redis_cluster:new(config)
    red:init_pipeline()

    --2)执行命令【set】
    red:set(key,value)
    red:commit_pipeline()

    --3)关闭链接
    red:close()
end


--创建查询数据get()
function lredis.get(key)
    --1)打开链接
    local red = redis_cluster:new(config)
    red:init_pipeline()

    --2)执行命令【set】
    red:get(key)
    local result = red:commit_pipeline()

    --3)关闭链接
    red:close()
    
    --4)返回结果集
    return result
end


return lredis

Lua脚本执行业务:

--多级缓存流程操作
--1)Lua脚本查询Nginx缓存
--2)Nginx如果没有缓存
--2.1)Lua脚本查询Redis
--2.1.1)Redis如果有数据,则将数据存入到Nginx缓存,并响应用户
--2.1.2)Redis没有数据,Lua脚本查询MySQL
--       MySQL有数据,则将数据存入到Redis、Nginx缓存[需要额外定义],响应用户
--3)Nginx如果有缓存,则直接将缓存响应给用户



--响应数据为JSON类型
ngx.header.content_type="application/json;charset=utf8"
--引入依赖库
--cjson:对象转JSON或者JSON转对象
local cjson = require("cjson")
local mysql = require("mysql")
local lrredis = require("redis")

--获取请求参数ID   http://192.168.211.141/act?id=1
local id = ngx.req.get_uri_args()["id"];

--加载本地缓存
local cache_ngx = ngx.shared.act_cache;

--组装本地缓存的key,并获取nginx本地缓存
local ngx_key = 'ngx_act_cache_'..id
local actCache = cache_ngx:get(ngx_key)

--如果nginx中没有缓存,则查询Redis集群缓存
if actCache == "" or actCache == nil then
    --从Redis集群中加载数据
    local redis_key = 'redis_act_'..id
    local result = lrredis.get(redis_key)
    
    --Redis中数据为空,查询数据库
    if result[1]==nil or result[1]==ngx.null then
        --组装SQL语句
        local sql = "select * from activity_info where id ="..id
        --执行查询
        result = mysql.query(sql)
        --数据不为空,则添加到Redis中
        if result[1]==nil or result[1]==ngx.null then
            ngx.say("no data")
        else
            --数据添加到Nginx缓存和Redis缓存
            lrredis.set(redis_key,cjson.encode(result))
            cache_ngx:set(ngx_key, cjson.encode(result), 2*60);
            ngx.say(cjson.encode(result))
        end
    else
        --将数据添加到Nginx缓存中
        cache_ngx:set(ngx_key, result, 2*60);
        --直接输出
        ngx.say(result)
    end
else
    --输出缓存数据
    ngx.say(actCache)
end

nginx配置:

#活动查询 
location /act  {     
    content_by_lua_file /usr/local/openresty/nginx/lua/activity.lua;
}

8.5 红包雨案例

8.5.1 红包场景概述

1. 抢红包的特点:

a 并发量大(抢红包,白捡的都去抢。所以人很多)
b 按照时间段来发放(生活中的红包就是在几个小时发几波)
c 抢的红包肯定是不能超过预设的总金额
e 抢红包肯定是先到先得。(抢红包的公平性)
f 在发红包时候,可以追加红包的数量和延迟抢红包时间。

2. 抢红包策略:
[站外图片上传中...(image-c0d688-1623382960683)]

老板规定发金额和发的个数确定好,通过算法得出每个红包金额,然后分批次将红包放入到redis中。每个人抢红包直接从redis中拿就可以了。因为redis是单线程所有每次只能一个用户取到,所以避免了一个红包多个人抢。传说中解决超卖。

8.5.2 红包放入缓存队列中

1. 定时将红包导入缓存队列:
初始化读取:

创建容器监听类 ,让该类实现接口 ApplicationListener ,当容器初始化完成后会调用onApplicationEvent 方法。然后去去到数据到redis队列中。
@Component 
public class MoneyPushTask implements  ApplicationListener<ContextRefreshedEvent>{ 
   @Override     
   public void onApplicationEvent(ContextRefreshedEvent event) {  
    //清空历史数据
    //加载新的数据
   }
}

定时加载:

可以使用定时任务,定时的更新红包数量

8.5.3 解决大量的人抢红包导致服务器崩溃

当有大量人去抢红包,服务器很有可能会崩溃。采用队列削峰。
[站外图片上传中...(image-fac8bc-1623382960683)]

大量用户来抢红包时候,使用Lua脚本将将请求放到缓存队列中,服务端处理队列中的请求。在lua脚本中可以导入 lua-resty-jwt模块,用来安全验证。

8.6 Nginx限流

我们采用多级缓存的模式,但是当用户反复刷新页面没有必要让所有请求到达服务器。还有一些恶意的攻击请求,也要避免请求到达服务器。限流是保护系统的一种方式。
1. 控制速率(控制请求数量和请求速度):
[站外图片上传中...(image-3f59f7-1623382960683)]

水过来先放到桶里,然后匀速的将水流出。当流入桶里水过大,水就直接溢出了。用户请求类似这样原理,当请求来了先放到缓冲中然后匀速的到达服务器,
当请求过多,直接拒绝请求。

nginx配置文件:

# 配置限制流缓存空间
limit_req_zone $binary_remote_addr zone=contentRateLimit:10m rate=2r/s; 
# 配置限流
limit_req zone=contentRateLimit;
# 上面参数的解释
binary_remote_addr 是一种key,表示基于 remote_addr(客户端IP) 来做限流,binary_ 的目的是压缩内存占用 量。 zone:定义共享内存区来存储
访问信息, contentRateLimit:10m 表示一个大小为10M,名字为contentRateLimit的 内存区域。1M能存储16000 IP地址的访问信息,10M可以存储
16W IP地址访问信息。 rate 用于设置大访问速率,rate=10r/s 表示每秒多处理10个请求。Nginx 实际上以毫秒为粒度来跟踪请求信息,因 此 10r/s
实际上是限制:每100毫秒处理一个请求。这意味着,自上一个请求处理完后,若后续100毫秒内又有请求到达,将 拒绝处理该请求.我们这里设置
成2 方便测试。 

注意:当设置了限流但是并发上来了,这样大部分请求都会被拒绝。

lilimit_req zone=contentRateLimit burst=4 nodelay; 

2. 控制并发连接数(限制某个ip连接服务器的个数,连接服务器的总数):

利用limit_conn_zone和limit_conn两个指令,限制某一个ip连接数。

nginx参数配置:

#根据IP地址来限制,存储内存大小10M 
limit_conn_zone $binary_remote_addr zone=addr:1m; 
limit_conn addr 2; 
# 参数解释
limit_conn_zone $binary_remote_addr zone=addr:10m;  表示限制根据用户的IP地址来显示,设置存储地址为的 内存大小10M 
limit_conn addr 2;   表示 同一个地址只允许连接2次。 

限制某个ip连接数量。限制连接的总个数

#IP限流 
limit_conn_zone $binary_remote_addr zone=perip:10m; 
#根据server的名字限流 
limit_conn_zone $server_name zone=perserver:10m; 

#单个客户端ip与服务器的连接数.
limit_conn perip 10; 
#限制与服务器的总连接数 
limit_conn perserver 100; 
上一篇下一篇

猜你喜欢

热点阅读