缓存穿透

缓存穿透是指查询一条数据库和缓存都没有的一条数据,就会一直查询数据库,对数据库的访问压力就会增大。

缓存穿透的解决方案,有以下两种:

  1. 缓存空对象:代码维护较简单,但是效果不好。
  2. 布隆过滤器:代码维护复杂,效果很好。

缓存空对象

缓存空对象是指当一个请求过来缓存中和数据库中都不存在该请求的数据,第一次请求就会跳过缓存进行数据库的访问,并且访问数据库后返回为空,此时也将该空对象进行缓存。

若是再次进行访问该空对象的时候,就会直接击中缓存,而不是再次数据库

redis-缓存穿透、缓存击穿、缓存雪崩 - 图1

存在的问题

若请求大量数据库为空的数据,会在缓存中存放大量的空值,占用内存空间,浪费资源,因此对于空对象,要设置一个较短的过期时间。

布隆过滤器

布隆过滤器是一种基于概率数据结构,主要用来判断某个元素是否在集合内,它具有运行速度快(时间效率),占用内存小的优点(空间效率),但是有一定的误识别率删除困难的问题。它只能告诉你某个元素一定不在集合内或可能在集合内。

布隆过滤器中引用了一个误判率的概念,即它可能会把不属于这个集合的元素认为可能属于这个集合,但是不会把属于这个集合的认为不属于这个集合,布隆过滤器的特点如下:

1、一个非常大的二进制位数组 (数组里只有0和1)

2、若干个哈希函数

3、空间效率查询效率高

4、不存在漏报(False Negative):某个元素在某个集合中,肯定能报出来。

5、可能存在误报(False Positive):某个元素不在某个集合中,可能也被爆出来。

6、不提供删除方法,代码维护困难。/7、位数组初始化都为0,它不存元素的具体值,当元素经过哈希函数哈希后的值(也就是数组下标)对应的数组位置值改为1。

redis-缓存穿透、缓存击穿、缓存雪崩 - 图2

初始化的布隆过滤器的结构图如下:

redis-缓存穿透、缓存击穿、缓存雪崩 - 图3

当一个数据进行存入布隆过滤器的时候,会经过若干个哈希函数进行哈希,得到对应的哈希值作为数组的下标,然后将初始化的位数组对应的下标的值修改为1,结果图如下:

redis-缓存穿透、缓存击穿、缓存雪崩 - 图4

当再次进行存入第二个值的时候,修改后的结果的原理图如下:

redis-缓存穿透、缓存击穿、缓存雪崩 - 图5

每次存入一个数据,就会哈希函数的计算,计算的结果就会作为下标,在布隆过滤器中有多少个哈希函数就会计算出多少个下标,布隆过滤器插入的流程如下:

  1. 将要添加的元素给m个哈希函数
  2. 得到对应于位数组上的m个位置
  3. 将这m个位置设为1

误判率

假设在多次存入值后,在布隆过滤器中存在x、y、z这三个值,布隆过滤器的存储结构图如下所示:

redis-缓存穿透、缓存击穿、缓存雪崩 - 图6

假设查询a这个数,实际中a这个数是不存在布隆过滤器中的,经过2个哈希函数计算后得到a的哈希值分别为2和13,结构原理图如下:

redis-缓存穿透、缓存击穿、缓存雪崩 - 图7

经过查询后,发现2和13位置所存储的值都为1,但是2和13的下标分别是x和z经过计算后的下标位置的修改,该布隆过滤器中实际不存在a,那么布隆过滤器就会误判改值可能存在,因为布隆过滤器不存元素值,所以存在误判率

布隆过布隆过滤的判断的准确率和以下两个因素有关:

  1. 布隆过滤器大小:越大,误判率就越小,所以说布隆过滤器一般长度都是非常大的。
  2. 哈希函数的个数:哈希函数的个数越多,那么误判率就越小。

为什么不能删除元素

删除元素后,将对应元素的下标设置为零,可能别的元素的下标也引用改下标,这样别的元素的判断就会收到影响,原理图如下:

redis-缓存穿透、缓存击穿、缓存雪崩 - 图8

当删除z元素之后,将对应的下标10和13设置为0,这样导致x和y元素的下标受到影响,导致数据的判断不准确,所以直接不提供删除元素的api。

布隆过滤器的使用

引入依赖:

  1. <dependency>
  2. <groupId>com.google.guava</groupId>
  3. <artifactId>guava</artifactId>
  4. <version>27.0.1-jre</version>
  5. </dependency>

有如下场景,要查询订单信息,首先先经过布隆过滤器进行过滤。

首先进行布隆过滤器的初始化。

  1. public static void MyBloomFilterSysConfig {
  2. @Autowired
  3. OrderMapper orderMapper
  4. // 1.创建布隆过滤器 第二个参数为预期数据量10000000,第三个参数为错误率0.00001
  5. BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("utf-8")),10000000, 0.00001);
  6. // 2.获取所有的订单,并将订单的id放进布隆过滤器里面
  7. List<Order> orderList = orderMapper.findAll()
  8. for (Order order;orderList ) {
  9. Long id = order.getId();
  10. bloomFilter.put("" + id);
  11. }
  12. }

在实际项目中会启动一个系统任务或者定时任务,来初始化布隆过滤器,将热点查询数据的id放进布隆过滤器里面,当用户再次请求的时候,使用布隆过滤器进行判断,改订单的id是否在布隆过滤器中存在,不存在直接返回null,具体操作代码:

  1. // 判断订单id是否在布隆过滤器中存在
  2. bloomFilter.mightContain("" + id)

布隆过滤器的缺点就是要维持容器中的数据,因为订单数据肯定是频繁变化的,实时的要更新布隆过滤器中的数据为最新。

缓存击穿

缓存击穿是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,瞬间对数据库的访问压力增大。

缓存击穿这里强调的是并发,造成缓存击穿的原因有以下两个:

  1. 该数据没有人查询过 ,第一次就大并发的访问。(冷门数据)
  2. 添加到了缓存,reids有设置数据失效的时间 ,这条数据刚好失效,大并发访问(热点数据)

对于缓存击穿的解决方案就是加锁,具体实现的原理图如下:

redis-缓存穿透、缓存击穿、缓存雪崩 - 图9

当用户出现大并发访问的时候,在查询缓存的时候和查询数据库的过程加锁,只能第一个进来的请求进行执行,当第一个请求把该数据放进缓存中,接下来的访问就会直接集中缓存,防止了缓存击穿

业界比价普遍的一种做法,即根据key获取value值为空时,锁上,从数据库中load数据后再释放锁。若其它线程获取锁失败,则等待一段时间后重试。

缓存雪崩

缓存雪崩 是指在某一个时间段,缓存集中过期失效。此刻无数的请求直接绕开缓存,直接请求数据库。

造成缓存雪崩的原因,有以下两种:

  1. reids宕机
  2. 大部分数据失效

redis-缓存穿透、缓存击穿、缓存雪崩 - 图10

redis-缓存穿透、缓存击穿、缓存雪崩 - 图11

对于缓存雪崩的解决方案有以下两种:

  1. 搭建高可用的集群,防止单机的redis宕机。
  2. 设置不同的过期时间,防止同意之间内大量的key失效。