快捷搜索:  汽车  科技

如何解决缓存性能衰退:缓存灾难问题解决

如何解决缓存性能衰退:缓存灾难问题解决布隆过滤器的巨大用处就是,能够迅速判断一个元素是否在一个集合中。因此他有如下三个使用场景:布隆过滤器:在一个存在一定数量的集合中过滤一个对应的数据,判断该数据是否在该集合中。上面查询最近红包记录提升用户访问效率,这种操作是一种正常操作,但也存在一些非正常操作,比如 wangwu 没有抢到红包,但用户恶意频繁去查询抢红包记录,此时Redis缓存中将一直没有数据,每次都会查询数据库,这种现象叫缓存穿透。缓存穿透该如何解决呢?我们提供这一种思路,如下图所示:防止缓存穿透,有多种方案,上面所实现的是一种最简单的方案,也有其他方案,比如布隆过滤器也是缓存穿透方案之一。布隆过滤器主要是解决大规模数据下不需要精确过滤的业务场景,如检查垃圾邮件地址,爬虫URL地址去重,解决缓存穿透问题等。

  • 缓存穿透解决方案
  • 理解布隆过滤器原理
  • 缓存击穿解决方案
  • 缓存雪崩解决方案
  • 缓存一致性解决方案
  • RESTful规范安全控制方案
1 缓存穿透实战

有些数据查询频率很高的时候,我们会将数据存入到缓存,用户每次查询直接查询缓存即可,从而提高用户访问数据的效率。

比如获取用户为 lisi 的抢红包记录,此时如果每次查询数据库效率都很低,我们可以第1次从数据库查询 lisi 最近的前10条抢红包记录,然后将记录存入到Redis缓存,下次直接查询redis缓存即可。

每次用户抢红包,谁抢到了红包,我们会将抢到红包的用户信息按照抢红包的金额大小的前100名用户信息公示出去,这里也可以采用这种方式来做。

当然,也不是所有数据都适合做缓存,需要根据数据特点来决定,如下图:

如何解决缓存性能衰退:缓存灾难问题解决(1)

1.1 缓存穿透介绍

如何解决缓存性能衰退:缓存灾难问题解决(2)

上面查询最近红包记录提升用户访问效率,这种操作是一种正常操作,但也存在一些非正常操作,比如 wangwu 没有抢到红包,但用户恶意频繁去查询抢红包记录,此时Redis缓存中将一直没有数据,每次都会查询数据库,这种现象叫缓存穿透。

缓存穿透该如何解决呢?我们提供这一种思路,如下图所示:

如何解决缓存性能衰退:缓存灾难问题解决(3)

1.2 穿透问题解决

如何解决缓存性能衰退:缓存灾难问题解决(4)

1.3 布隆过滤器

防止缓存穿透,有多种方案,上面所实现的是一种最简单的方案,也有其他方案,比如布隆过滤器也是缓存穿透方案之一。布隆过滤器主要是解决大规模数据下不需要精确过滤的业务场景,如检查垃圾邮件地址,爬虫URL地址去重,解决缓存穿透问题等。

布隆过滤器:在一个存在一定数量的集合中过滤一个对应的数据,判断该数据是否在该集合中。

1.3.1 原理

布隆过滤器的巨大用处就是,能够迅速判断一个元素是否在一个集合中。因此他有如下三个使用场景:

  1. 网页爬虫对URL的去重,避免爬取相同的URL地址
  2. 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱(同理,垃圾短信)
  3. 缓存穿透,将所有可能存在的数据缓存放到布隆过滤器中,当黑客访问不存在的缓存时迅速返回避免缓存及DB挂掉。

我们来谈谈布隆过滤器的原理

其内部维护一个全为0的bit数组,需要说明的是,布隆过滤器有一个误判率的概念,误判率越低,则数组越长,占空间越大。误判率越高则数组越小,所占的空间越小。

假设,根据误判率,我们生成一个10位的bit数组,以及2个hash函数((f_1 f_2)),如下图所示(生成的数组的位数和hash函数的数量,我们不用去关心是如何生成的,有数学论文进行过专业的证明)。

如何解决缓存性能衰退:缓存灾难问题解决(5)

假设输入集合为((N_1 N_2)) 经过计算(f_1(N_1))得到的数值得为2,(f_2(N_1))得到的数值为5,则将数组下标为2和下表为5的位置置为1,如下图所示

如何解决缓存性能衰退:缓存灾难问题解决(6)

同理,经过计算(f_1(N_2))得到的数值得为3,(f_2(N_2))得到的数值为6,则将数组下标为3和下表为6的位置置为1,如下图所示

如何解决缓存性能衰退:缓存灾难问题解决(7)

这个时候,我们有第三个数(N_3),我们判断(N_3)在不在集合((N_1 N_2))中,就进行(f_1(N_3),f_2(N_3))的计算

  1. 若值恰巧都位于上图的红色位置中,我们则认为,(N_3)在集合((N_1 N_2))中
  2. 若值有一个不位于上图的红色位置中,我们则认为,(N_3)不在集合((N_1 N_2))中

以上就是布隆过滤器的计算原理。

1.3.2 布隆过滤器案例

引入依赖包

如何解决缓存性能衰退:缓存灾难问题解决(8)

编写测试类

如何解决缓存性能衰退:缓存灾难问题解决(9)

如何解决缓存性能衰退:缓存灾难问题解决(10)

我们可以发现有330个被误判 误判的概率为0.03,源码中也有说明。

如何解决缓存性能衰退:缓存灾难问题解决(11)

这里的误判概率是可以调整的,每次创建 BloomFilter 的时候,指定误判概率值即可,这个值必须大于0。

优点

  1. 思路简单
  2. 保证一致性
  3. 性能强

缺点

  1. 代码复杂度增大
  2. 需要另外维护一个集合来存放缓存的Key
  3. 布隆过滤器不支持删值操作
2 缓存击穿实战

上面案例我们实现了某个用户抢红包的信息查询,接下来我们实现公示抢到红包并且按照红包金额大小排序查询出前100名用户信息,这块数据并发量将更大,我们需要做缓存处理。

2.1 抢红包排行查询

如何解决缓存性能衰退:缓存灾难问题解决(12)

Service 这里做了缓存操作,缓存1分钟,1分钟过后,会再次查询数据库

如何解决缓存性能衰退:缓存灾难问题解决(13)

测试结果如下:

如何解决缓存性能衰退:缓存灾难问题解决(14)

2.2 击穿现象分析

如何解决缓存性能衰退:缓存灾难问题解决(15)

我们先来了解下缓存击穿,缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

上面查询红包排名就存在击穿现象,比如10万用户请求,此时缓存刚好过期,10万用户同时到达了第②个步骤,而且此时Redis中都判断没有数据,那么此时就都查询数据库,给数据库带来巨大的压力,甚至是宕机。

2.3 击穿解决方案

针对缓存击穿现象,可以有多重解决方案。我们这里给大家讲解一下实用的5种方案。

2.3.1 定时器

后台定义一个job(定时任务)专门主动更新缓存数据.比如 一个缓存中的数据过期时间是1分钟 那么job每隔25秒刷新数据(将从数据库中查到的数据更新到缓存中),或者缓存不过期,直接写定时任务定时更新即可。定时器需要择优选择,比如可以用 elastic-job xxl-job 。

这种方案比较容易理解,但会增加系统复杂度。比较适合那些 key 相对固定 cache 粒度较大的业务,key 比较分散的则不太适合,实现起来也比较复杂。

2.3.2 多级缓存

如何解决缓存性能衰退:缓存灾难问题解决(16)

采用多级缓存也可以有效防止击穿现象,首先通过程序将缓存存入到Redis缓存,且永不过期,用户查询的时候,先查询nginx缓存,如果Nginx缓存没有,则查询Redis缓存,并将Redis缓存存入到Nginx一级缓存中,并设置更新时间。这种方案不仅可以提升查询速度,同时又能防止击穿问题,并且提升了程序的抗压能力。

2.3.3 分布式锁

解决上面超卖问题,我们可以采用分布式锁来控制,分布式锁的原理很简单。

分布式锁主要是实现在分布式场景下保证数据的最终一致性。在单进程的系统中,存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步(lock—synchronized),使其在修改这种变量时能够线性执行消除并发修改变量。但分布式系统是多部署、多进程的,开发语言提供的并发处理API在此场景下就无能为力了。

目前市面上分布式锁常见的实现方式有三种:

如何解决缓存性能衰退:缓存灾难问题解决(17)

大部分网站使用的分布式锁是基于缓存的,有更好的性能,而缓存一般是以集群方式部署,保证了高可用性。而Redis分布式锁官方推荐使用redisson。

Redission分布式锁说明:

如何解决缓存性能衰退:缓存灾难问题解决(18)

基于Redisson分布式锁实现

步骤:

如何解决缓存性能衰退:缓存灾难问题解决(19)

1)引入依赖

如何解决缓存性能衰退:缓存灾难问题解决(20)

2)锁操作方法实现

要想用到分布式锁,我们就必须要实现获取锁和释放锁,获取锁和释放锁可以编写一个 DistributedLocker 接口,代码如下:

如何解决缓存性能衰退:缓存灾难问题解决(21)

如何解决缓存性能衰退:缓存灾难问题解决(22)

实现上面接口中对应的锁管理方法 编写一个锁管理类 RedissonDistributedLocker ,代码如下:

如何解决缓存性能衰退:缓存灾难问题解决(23)

如何解决缓存性能衰退:缓存灾难问题解决(24)

如何解决缓存性能衰退:缓存灾难问题解决(25)

3)配置Redis链接

在resources下新建文件 redisson.yml ,主要用于配置redis集群节点链接配置,代码如下:

如何解决缓存性能衰退:缓存灾难问题解决(26)

如何解决缓存性能衰退:缓存灾难问题解决(27)

4)创建Redisson管理对象

Redisson管理对象有2个,分别为 RedissonClient 和 RedissonConnectionFactory ,我们只用在项目的

RedisConfig 中配置一下这2个对象即可,在 RedisConfig 中添加的代码如下:

如何解决缓存性能衰退:缓存灾难问题解决(28)

5)分布式锁实现

如何解决缓存性能衰退:缓存灾难问题解决(29)

2.3.4 队列术

如何解决缓存性能衰退:缓存灾难问题解决(30)

上面我们已经使用过队列术了,队列术在面对零点洪峰流量时,是相当有效,可以直接将所有流量存入到队列中,让后台不用同时处理很多请求,而是从队列中逐个消费逐个处理,上图是实现流程,由于前面抢单已经实现过该流程,所以这里不再重复讲解。

基于Nginx缓存队列术

如何解决缓存性能衰退:缓存灾难问题解决(31)

针对一些特定操作,如果请求并发量极高,我们可以采用Nginx自身的队列术,在上一章我们已经学过了Nginx的代理缓存,其中有一个属性叫 proxy_cache_lock ,该属性的意思是:当多个客户端请求一个缓存中不存在的文件(或称之为一个MISS),只有这些请求中的第一个被允许发送至服务器。其他请求在第一个请求得到满意结果之后在缓存中得到文件。如果不启用 proxy_cache_lock ,则所有在缓存中找不到文件的请求都会直接与服务器通信。

proxy_cache_lock 的作用其实和队列术及其类似,只不过发生的地方以及处理的语言不同而已。我们正好可以使用代理缓存来处理一些查询量大的相同数据,例如查询抢红包Top100就可以用Nginx的代理缓存中proxy_cache_lock 来实现。

如何解决缓存性能衰退:缓存灾难问题解决(32)

后台代码移除分布式锁:

如何解决缓存性能衰退:缓存灾难问题解决(33)

此时只有第1次从后台获取数据,当然这里会请求3次后才会从缓存拿数据,因为有个属性 proxy_cache_min_use 3属性。

3 缓存雪崩解决方案

缓存雪崩介绍

缓存雪崩是指,由于缓存层承载着大量请求,有效的保护了存储层,但是如果缓存层由于某些原因整体不能提供服务,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

解决方案

1)缓存高可用

即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,比如 Redis Sentinel 和 Redis Cluster 都实现了高可用。

2)限流

微服务网关或者Nginx做好限流操作,防止大量请求直接进入后端,使后端载荷过重最后宕机。

3)数据预热

预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀,不要同时失效。

4)队列术限流

使用Nginx队列或者MQ队列,缓存用户的请求,让所有相同操作只有1次查询数据库,并将查询的数据加入到缓存中,下次查询直接从缓存中获取数据。

5)加锁

数据操作,如果是带有缓存查询的,均使用分布式锁,防止大量请求直接操作数据库。

6)多级缓存(推荐)

采用多级缓存,Nginx Redis MyBatis二级缓存,当Nginx缓存失效时,查找Redis缓存,Redis缓存失效查找MyBatis二级缓存。

4 缓存一致性

用户每次抢完红包,要查看自己抢红包记录,此时需要查询数据库表 money_log 如果每次都查询 money_log 就会占用大量数据库资源。此时我们应该将数据存储到缓存中,每次查询直接从缓存获取即可。

但现在面临的问题是如果用户抢到了不同的红包,缓存没法及时更新,因此我们需要实现抢红包数据库数据和Redis缓存中的数据同步。

4.1 缓存一致性解决方案

如何解决缓存性能衰退:缓存灾难问题解决(34)

用户每次操作数据库的时候,使用Canal监听数据库指定表的增量变化,在Java程序中消费Canal监听到的增量变化,并在Java程序中实现对Redis缓存或者Nginx缓存的更新。

用户查询的时候,先通过Lua查询Nginx的缓存,如果Nginx缓存没有数据,则查询Redis缓存,Redis缓存如果也没有数据,可以去数据库查询.

4.2 Canal介绍

Canal主要用途是基于 MySQL 数据库增量日志解析,并能提供增量数据订阅和消费,应用场景十分丰富。

github地址:https://github.com/alibaba/canal

版本下载地址:https://github.com/alibaba/canal/releases

文档地址:https://github.com/alibaba/canal/wiki/Docker-QuickStart

如何解决缓存性能衰退:缓存灾难问题解决(35)

Canal应用场景

1.电商场景下商品实时更新同步到至Elasticsearch、solr等搜索引擎; 2.价格、库存发生变更实时同步到redis; 3.数据库异地备份、数据同步; 4.代替使用轮询数据库方式来监控数据库变更,有效改善轮询耗费数据库资源。

MySQL主从复制原理

  1. MySQL master 将数据变更写入二进制日志( binary log 其中记录叫做二进制日志事件 binary log events ,可以通过 show binlog events 进行查看)
  1. MySQL slave 将 master 的 binary log events 拷贝到它的中继日志( relay log ) 3. MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据

Canal工作原理

  1. canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
  2. MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal ) 3.canal 解析 binary log 对象(原始为 byte流)

如何解决缓存性能衰退:缓存灾难问题解决(36)

4.3 Canal配置

1)开启MySQL的bin-log

开启MySQL的binlog日志功能:

如何解决缓存性能衰退:缓存灾难问题解决(37)

2)Canal安装

这里采用容器安装

如何解决缓存性能衰退:缓存灾难问题解决(38)

配置CanalServer

修改 /home/admin/canal-server/conf/canal.properties 将它的id属性修改成和mysql数据库中server-id不同的值,如下图:

如何解决缓存性能衰退:缓存灾难问题解决(39)

修改 /home/admin/canal-server/conf/example/instance.properties 配置要监听的数据库服务地址和监听数据变化的数据库以及表,修改如下:

如何解决缓存性能衰退:缓存灾难问题解决(40)

指定监听数据库表的配置如下 canal.instance.filter.regex :

如何解决缓存性能衰退:缓存灾难问题解决(41)

重启canal:

如何解决缓存性能衰退:缓存灾难问题解决(42)

不要忘了MySQL创建账号并授权:

如何解决缓存性能衰退:缓存灾难问题解决(43)

4.4 同步更新Redis缓存

修改 com.itheima.service.impl.MoneyLogServiceImpl ,添加方法 list(String username) ,代码如下:

如何解决缓存性能衰退:缓存灾难问题解决(44)

创建类 com.itheima.canal.MoneyLogSync ,实现消费Canal中监听到的增量数据,代码如下:

如何解决缓存性能衰退:缓存灾难问题解决(45)

application.yml中配置Canal的地址:

如何解决缓存性能衰退:缓存灾难问题解决(46)

关于微服务中如何消费Canal监听到的数据,参考参考地址: https://github.com/NormanGyllenhaal/canal-client

我们可以测试实现抢单,抢单后,数据会自动同步到Redis缓存中。

如何解决缓存性能衰退:缓存灾难问题解决(47)

4.5 清理Nginx缓存

清理Nginx缓存,可以利用 purge 来清理 请求地址: <http://192.168.211.141/purge/mlog/top> 。

如何解决缓存性能衰退:缓存灾难问题解决(48)

我们现在要用程序访问,这里需要做相关操作,在java代码中访问上面地址即可 在这里大家可以把前面所学的基于Jwt令牌身份安全校验弄进去,实现安全清理缓存,也可以基于Nginx安全配置实现缓存清理操作。

如何解决缓存性能衰退:缓存灾难问题解决(49)

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

当前主流架构都属于微服务架构,主流的开发模式是前后端分离,前后端分离有较强的数据规则,前面针对缓存击穿和穿透解决方案适用于普通项目,不约束项目数据规则,但针对规则如此强的前后端分离开发模式和RESTful的微服务架构,应该是有更优秀的解决方案,不仅限于缓存穿透、缓存击穿问题解决。

解决基于RESTful开发的站点的安全风控解决方案。【缓存穿透】、【缓存击穿】、【缓存雪崩】、【黑白名单】、【定向日志收集】、【防止攻击】、【限流】、【熔断】

5.1 RESTful特性分析

刚才我们做了一个分析,所有前后端分离项目数据规则极强,我们设计了一套如下安全控制解决方案。

如何解决缓存性能衰退:缓存灾难问题解决(50)

我们如果能在Nginx端使用Lua脚本取代Nginx自发执行操作,可以实现很多高级功能。按照上面架构图规则可以过滤很多不安全因素,实现一些高级功能,例如:限流、故障自动切换、缓存击穿、缓存穿透、用户行为收集等,所有能做的事几乎都能做,我们这里拿恶意请求为例,可以制作出如下高质量的功能。

如何解决缓存性能衰退:缓存灾难问题解决(51)

5.2 功能实现分析

如何解决缓存性能衰退:缓存灾难问题解决(52)

1)无效路径收集

如上图,如果我们能在Nginx执行Http请求,并获取Http请求后的结果,如果请求后的结果是404,证明地址无效,我们可以把该请求地址存储到缓存中,标记为无效标识,下次有相同地址请求,直接跳转到404界面,而不需要请求后端微服务。

2)缓存击穿

定期检测缓存过期时间,一旦要过期,立即将请求做队列限流控制。

3)黑白名单过滤

导入黑白名单IP,每次获取用户IP,检测用户IP是否为黑名单IP,一旦为黑名单IP,则直接拒绝访问。

4)异常熔断降级

收集指定路径返回数据,返回code=500的链接指定时间内频率超过N,则将该路径链接存入缓存,一段时间内禁止用户访问,防止大量错误连接导致服务器宕机。

5.3 Lua执行Http请求5.3.1 响应接口设计

1)规范接口设计

响应数据必须是规范的数据,我们可以为后端设计一个响应数据封装对象,代码如下:

如何解决缓存性能衰退:缓存灾难问题解决(53)

2)请求测试

创建测试 RestUserController ,代码如下:

如何解决缓存性能衰退:缓存灾难问题解决(54)

测试: <http://192.168.211.141/api>

如何解决缓存性能衰退:缓存灾难问题解决(55)

5.3.2 Lua执行Http请求实现

lua执行Http请求,需要依赖http模块,Http模块下载地址 https://github.com/ledgetech/lua-resty-http 下载该模块后,解压到 /usr/local/openresty/lua-resty-http-master 下,并且在nginx的nginx.conf中指定依赖库地址 lua_package_path "/usr/local/openresty/nginx/lua/?.lua;/usr/local/openresty/lua-resty-http-master/lib/?.lua;/usr/local/openresty/lua-resty-jwt-master/lib/resty?.lua;;";

编写lua脚本用于处理用户请求, resthttp.lua 脚本如下:

如何解决缓存性能衰退:缓存灾难问题解决(56)

修改nginx.conf,添加如下配置:

如何解决缓存性能衰退:缓存灾难问题解决(57)

访问 <http://192.168.211.141/api> 效果如下:

如何解决缓存性能衰退:缓存灾难问题解决(58)

此时如果我们在 resthttp.lua 织入缓存脚本,可以实现各种条件判断,前面已经实现过相关操作,这里我们就不做演示了。

如果想把地址换成动态的,可以获取用户访问地址,这里提供了一些关于Http参数处理的操作,大家可以参考实现想要的功能。

如何解决缓存性能衰退:缓存灾难问题解决(59)

猜您喜欢: