目录


1 缓存使用场景

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

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

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

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

缓存穿透场景解决与布隆过滤器的使用_redis

2 缓存穿透介绍

缓存穿透场景解决与布隆过滤器的使用_高可用_02

上面查询最近红包记录提升用户访问效率,这种操作是一种正常操作,但也存在一些非正常操作,比如 wangwu 没有抢到红包,​​但用户恶意频繁去查询抢红包记录,此时Redis缓存中将一直没有数据,每次都会查询数据库,这种现象叫缓存穿透。​​缓存穿透该如何解决呢?我们提供这一种思路,如下图所示:

缓存穿透场景解决与布隆过滤器的使用_数据_03

3 穿透问题解决

缓存穿透场景解决与布隆过滤器的使用_布隆过滤器_04

4 布隆过滤器

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

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

4.1 原理

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

  1. 网页爬虫对​​URL的去重​​,​​避免爬取相同的URL地址​
  2. 反垃圾邮件,从数十亿个垃圾邮件列表中​​判断某邮箱是否垃圾邮箱​​(同理,垃圾短信)
  3. 缓存穿透,将所有​​可能存在的数据缓存放到布隆过滤器中​​,当访问不存在的缓存时迅速返回避免缓存及DB挂掉。
    我们来谈谈布隆过滤器的原理
    其内部维护一个全为0的bit数组,需要说明的是,布隆过滤器有一个误判率的概念,​​误判率越低,则数组越长,所占空间越大。误判率越高则数组越小,所占的空间越小。​​ 假设,根据误判率,我们生成一个10位的bit数组,以及2个hash函数((f_1,f_2)),如下图所示(生成的数组的位数和hash函数的数量,我们不用去关心是如何生成的,有数学论文进行过专业的证明)。
    缓存穿透场景解决与布隆过滤器的使用_高可用_05
    假设输入集合为((N_1,N_2)),经过计算(f_1(N_1))得到的数值得为2,(f_2(N_1))得到的数值为5,则将数组下标为2和下表为5的位置置为1,如下图所示
    缓存穿透场景解决与布隆过滤器的使用_缓存_06
    同理,经过计算(f_1(N_2))得到的数值得为3,(f_2(N_2))得到的数值为6,则将数组下标为3和下表为6的位置置为1,如下图所示
    缓存穿透场景解决与布隆过滤器的使用_redis_07
    这个时候,我们有第三个数(N_3),我们判断(N_3)在不在集合((N_1,N_2))中,就进行(f_1(N_3),f_2(N_3))的计算
  4. 若值恰巧都位于上图的红色位置中,我们则认为,(N_3)在集合((N_1,N_2))中
  5. 若值有一个不位于上图的红色位置中,我们则认为,(N_3)不在集合((N_1,N_2))中
    以上就是布隆过滤器的计算原理。

4.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​​,源码中也有说明。

缓存穿透场景解决与布隆过滤器的使用_数据_08

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

缓存穿透场景解决与布隆过滤器的使用_布隆过滤器_09

优点

  1. 思路简单
  2. 保证一致性
  3. ​性能强​​ 缺点
  4. 代码复杂度增大
  5. 需要​​另外维护一个集合来存放缓存的Key​
  6. 布隆过滤器不支持​​删值操作​