概述
缓存灾难问题
缓存穿透
有些数据查询频率很高的时候,我们会将数据存入到缓存,用户每次查询直接查询缓存即可,从而提高用户访问数据的效率。
比如获取用户为 a 的抢红包记录,此时如果每次查询数据库效率都很低,我们可以第1次从数据库查询 a 最近的前10条抢红包记录,然后将记录存入到Redis缓存,下次直接查询Redis缓存即可。
每次用户抢红包,谁抢到了红包,我们会将抢到红包的用户信息按照抢红包的金额大小的前100名用户信息公示出去,这里也可以采用这种方式来做。
当然,也不是所有数据都适合做缓存,需要根据数据特点来决定,如下图:
缓存穿透介绍
上面查询最近红包记录提升用户访问效率,这种操作是一种正常操作,但也存在一些非正常操作,比如 wangwu没有 抢到红包,但用户恶意频繁去查询抢红包记录,此时Redis缓存中将一直没有数据,每次都会查询数据库,这种现象 叫缓存穿透。
穿透问题解决
缓存穿透该如何解决呢?我们提供这一种思路,如下图所示:
布隆过滤器
防止缓存穿透,有多种方案,上面所实现的是一种最简单的方案,也有其他方案,比如布隆过滤器也是缓存穿透方案之一。布隆过滤器主要是解决大规模数据下不需要精确过滤的业务场景,如检查垃圾邮件地址,爬虫URL地址去重, 解决缓存穿透问题等。
布隆过滤器:在一个存在一定数量的集合中过滤一个对应的数据,判断该数据是否在该集合中。
原理
布隆过滤器的巨大用处就是,能够迅速判断一个元素是否在一个集合中。因此他有如下三个使用场景:
- 网页爬虫对URL的去重,避免爬取相同的URL地址
- 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱(同理,垃圾短信)
- 缓存穿透,将所有可能存在的数据缓存放到布隆过滤器中,当黑客访问不存在的缓存时迅速返回避免缓存及DB 挂掉。
我们来谈谈布隆过滤器的原理:
其内部维护一个全为0的bit数组,需要说明的是,布隆过滤器有一个误判率的概念,误判率越低,则数组越长,所占 空间越大。误判率越高则数组越小,所占的空间越小。
假设,根据误判率,我们生成一个10位的bit数组,以及2个hash函数((f_1,f_2)),如下图所示(生成的数组的位数和 hash函数的数量,我们不用去关心是如何生成的,有数学论文进行过专业的证明)。
假设输入集合为((N_1,N_2)),经过计算(f_1(N_1))得到的数值得为2,(f_2(N_1))得到的数值为5,则将数组下标为2和下表为5的位置置为1,如下图所示
同理,经过计算(f_1(N_2))得到的数值得为3,(f_2(N_2))得到的数值为6,则将数组下标为3和下表为6的位置置为1, 如下图所示
这个时候,我们有第三个数(N_3),我们判断(N_3)在不在集合((N_1,N_2))中,就进行(f_1(N_3),f_2(N_3))的计算 - 若值恰巧都位于上图的红色位置中,我们则认为,(N_3)在集合((N_1,N_2))中
- 若值有一个不位于上图的红色位置中,我们则认为,(N_3)不在集合((N_1,N_2))中
以上就是布隆过滤器的计算原理。
布隆过滤器案例
引入依赖包
<!--google的布隆过滤器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>22.0</version>
</dependency>
编写测试类
public class BloomFilterTest {
private static int size = 1000000;
//Google的布隆过滤器
private static BloomFilter<Integer> bloomFilter
=BloomFilter.create(Funnels.integerFunnel(), size,0.0001);
public static void main(String[] args) {
//放一百万个key到布隆过滤器中 1000000
for (int i = 0; i < size; i++) {
bloomFilter.put(i);
}
List<Integer> list = new ArrayList<Integer>(1000);
//取10000个不在过滤器里的值,看看有多少个会被认为在过滤器里
for (int i = size + 10000; i < size + 20000; i++) {
if (bloomFilter.mightContain(i)) {
list.add(i);
}
}
System.out.println("误判的数量:" + list.size());
}
}
我们可以发现有330个被误判,误判的概率为0.03,源码中也有说明。
这里的误判概率是可以调整的,每次创建 BloomFilter的时候,指定误判概率值即可,这个值必须大于0。
优点
- 思路简单
- 保证一致性
- 性能强
缺点 - 代码复杂度增大
- 需要另外维护一个集合来存放缓存的Key
- 布隆过滤器不支持删值操作
缓存击穿
缓存击穿介绍
我们先来了解下缓存击穿,缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
上面查询红包排名就存在击穿现象,比如10万用户请求,此时缓存刚好过期,10万用户同时到达了第②个步骤,而且此时Redis中都判断没有数据,那么此时就都查询数据库,给数据库带来巨大的压力,甚至是宕机。
击穿解决方案
针对缓存击穿现象,可以有多重解决方案。我们这里给大家讲解一下实用的5种方案。
定时器
后台定义一个job(定时任务)专门主动更新缓存数据.比如,一个缓存中的数据过期时间是1分钟,那么job每隔25秒刷 新数据(将从数据库中查到的数据更新到缓存中),或者缓存不过期,直接写定时任务定时更新即可。定时器需要择优 选择,比如可以用 elastic-job, xxl-job。
这种方案比较容易理解,但会增加系统复杂度。比较适合那些 key 相对固定,cache 粒度较大的业务,key 比较分散的 则不太适合,实现起来也比较复杂。
多级缓存
采用多级缓存也可以有效防止击穿现象,首先通过程序将缓存存入到Redis缓存,且永不过期,用户查询的时候,先 查询Nginx缓存,如果Nginx缓存没有,则查询Redis缓存,并将Redis缓存存入到Nginx一级缓存中,并设置更新时 间。这种方案不仅可以提升查询速度,同时又能防止击穿问题,并且提升了程序的抗压能力。
分布式锁
利用分布式锁,进行代码块锁操作,使得同时只有一个请求落到数据库,避免击穿风险。
分布式锁主要是实现在分布式场景下保证数据的最终一致性。在单进程的系统中,存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步(lock—synchronized),使其在修改这种变量时能够线性执行消除并发修改变量。但分布式系统是多部署、多进程的,开发语言提供的并发处理API在此场景下就无能为力了。
目前市面上分布式锁常见的实现方式有三种:
- 基于数据库实现分布式锁;(性能效率比较低,不推荐)
- 基于缓存(Redis等)实现分布式锁; (推荐)
- 基于Zookeeper实现分布式锁;(推荐)
大部分网站使用的分布式锁是基于缓存的,有更好的性能,而缓存一般是以集群方式部署,保证了高可用性。而Redis分布式锁官方推荐使用redisson。
队列术
针对一些特定操作,如果请求并发量极高,我们可以采用Nginx自身的队列术,在上一章我们已经学过了Nginx的代 理缓存,其中有一个属性叫 proxy_cache_lock,该属性的意思是:当多个客户端请求一个缓存中不存在的文件(或 称之为一个MISS),只有这些请求中的第一个被允许发送至服务器。其他请求在第一个请求得到满意结果之后在缓 存中得到文件。如果不启用 proxy_cache_lock,则所有在缓存中找不到文件的请求都会直接与服务器通信。 proxy_cache_lock的作用其实和队列术及其类似,只不过发生的地方以及处理的语言不同而已。
我们正好可以使用代理缓存来处理一些查询量大的相同数据,例如查询抢红包Top100就可以用Nginx的代理缓存中 proxy_cache_lock来实现。
缓存雪崩
缓存雪崩介绍
缓存雪崩是指,由于缓存层承载着大量请求,有效的保护了存储层,但是如果缓存层由于某些原因整体不能提供服 务,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
解决方案
缓存高可用
即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,比如 Redis Sentinel 和 Redis Cluster 都实现了高 可用。
限流
微服务网关或者Nginx做好限流操作,防止大量请求直接进入后端,使后端载荷过重最后宕机。
数据预热
预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀,不要同时失效。
队列术限流
使用Nginx队列或者MQ队列,缓存用户的请求,让所有相同操作只有1次查询数据库,并将查询的数据加入到缓存 中,下次查询直接从缓存中获取数据。
加锁
数据操作,如果是带有缓存查询的,均使用分布式锁,防止大量请求直接操作数据库。
多级缓存(推荐)
采用多级缓存,Nginx+Redis+MyBatis二级缓存,当Nginx缓存失效时,查找Redis缓存,Redis缓存失效查找 MyBatis二级缓存。
最后
以上就是欣慰红酒为你收集整理的深入浅出缓存设计---6、缓存灾难问题缓存灾难问题的全部内容,希望文章能够帮你解决深入浅出缓存设计---6、缓存灾难问题缓存灾难问题所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复