背景
如果我们使用缓存,那样会带来缓存三大问题,缓存穿透、缓存雪崩、缓存击穿。这里针对缓存穿透并使用布隆过滤器解决。
缓存穿透就是有心用户利用缓存和数据库都必不存在的数据来发送恶意请求,从而绕过缓存,直接访问数据库,最终导致数据库崩溃的问题。
这是一个通用的问题,关键就在于我们怎么知道请求的 key 在我们的数据库里面是否存在,如果数据量特别大的话,我们怎么去快速判断。
这个问题是如何在海量元素中(例如 10 亿无序、不定长、不重复)快速判断一个元素是否存在。
这个问题涉及两个关键点:海量数据、快速判断。
如果我们直接把这些元素的值放到基本的数据结构Set里面,会十分占用空间。
所以,我们存储这几十亿个元素,不能直接存值,我们应该找到一种最简单的最节省空间的数据结构,用来标记这个元素有没有出现。这个东西我们就把它叫做位图,他是一个有序的数组,只有两个值,0 和 1。0 代表不存在,1 代表存在。
要让这个数组标记这些元素是否存在,必须有一个映射方法。
这个映射方法需要符合以下基本要求:
1)因为值长度是不固定的,所以希望不同长度的输入,可以得到固定长度的输出。
2)转换成下标的时候,希望在这个有序数组里面是分布均匀的,不然的话全部挤到一对去了,我也没法判断到底哪个元素存了,哪个元素没存。
结合上面两个要求,使用分布性优良哈希函数加上相应取模方法,可以得到相应下标。

具体如上图,数据经过哈希计算并取模得到相应的数组下标,并把该下标值置1,表示存在。
然后到时判断数据是否存在时,只需要把数据用相应的函数计算出下标,再查看对应数据元素是否为1,1则存在,0则不存在。
由于会出现哈希碰撞,此时YaoMing和Kobe Bryant计算出了相同的下标。所以此时使用该方法判断数据是否存在就会出现误差,比如假如Kobe Bryant实际上是不存在,但是YaoMing数据已经把下标6的元素置1了,然后Kobe Bryant经过运算得到下标为6,此时他去查看6元素是否为1,因为YaoMing已经把他置1了,所以会判断Kobe Bryant是存在,但是实际上它是不存在的。
因为哈希冲突会导致判断处弱,所以要尽量减少哈希冲突的概率。方法有:
- 增大位图数组的容量,因为我们的函数是分布均匀的,所以,位图容量越大,在同一个位置发生哈希碰撞的概率就越小,但是位图数组容量增大意味着会增大内存的消耗,所以不能不讲道理地扩大位图容量,应该是在错误率和位图容量中平衡取值。
- 如果数据经过一次哈希计算,得到的相同下标的概率比较高,所以可以计算多次呢? 原来只用一个哈希函数,现在对于每一个要存储的元素都用多个哈希函数计算,这样每次计算出来的下标都相同的概率就小得多了。但是 我们也不讲道理地使用很多次的哈希计算函数,因为很多次的哈希计算会消耗掉cpu的性能,和延长判断速度。
所以总的来说,我们既要节省空间,又要很高的计算效率,就必须在位图容量和函数个数之间找到一个最佳的平衡。
对于如何取得平衡,这个事情早就有人研究过了,在 1970 年的时候,有一个叫做布隆的前辈对于判断海量元素中元素是否存在的问题进行了研究,也就是到底需要多大的位图容量和多少个哈希函数,它发表了一篇论文,提出的这个容器就叫做布隆过滤器。
但是无论如果也不可能达到100%正确率,除非使用绝对均匀的下标算法和绝对大于元素个数且随时扩容的位数组。
所以,这个是布隆过滤器的一个很重要的特性,因为哈希碰撞不可避免,所以它会存在一定的误判率。这种把本来不存在布隆过滤器中的元素误判为存在的情况,我们把它叫做假阳性(False Positive Probability,FPP)。
布隆过滤器的原理就是跟上面讲到的原理是一样的。
布隆过滤器的特点:
容器角度:
- 如果布隆过滤器判断结果为元素存在,那么该元素实际上元素不一定会存在,由于哈希碰撞,所以会存在一定误判率,上面已经说明了。
- 如果布隆过滤器判断结果为元素不存在,那么他就一定不存在,因为无论哈希碰撞啥的,只要该元素计算出下标值对应数组元素值为0,那么该元素就必定不存在,自己想想就好,只可意会不可言传。
- 布隆过滤器是不支持删除元素的,因为如果位图的某个位被多个元素占用着,那么如果删除其中一个元素是否能将该位置0能,置0的话会影响到其他元素,不置0就等于没删除。
元素角度:
- 如果元素实际不存在,布隆过滤器可能判断存在。
- 如果元素实际存在,布隆过滤器一定判断存在。
利用第二个特性,我们就能解决持续从数据库查询不存在的值的问题,把要查询的值先过布隆过滤器,判断是否存在,存在就走redis缓存,不存在就直接返回,并且配合缓存空值,可以有效解决缓存穿透问题,虽然存在一定误差,但是在业务范围内允许接受。

- 第一步先查询数据库数据并加入到布隆过滤器中。
- 请求发送过来布隆过滤器判断是否命中,命中就走缓存,之后接着看是否走数据库还是直接从缓存获取返回。
- 如果布隆过滤器miss,就直接返回,不走cache了。
布隆过滤器实战:
谷歌的 Guava 里面就提供了一个现成的布隆过滤器。
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.1-jre</version>
</dependency>
/**
* @author YeHaocong
* @decription
* 测试布隆过滤器的正确判断和误判
*
* 往布隆过滤器里面存放100万个元素
* 测试100个存在的元素和9900个不存在的元素
*
*/
public class BloomFilterDemo {
//元素个数 100万
private static final int insertions = 1000000;
public static void main(String[] args) {
//创建一个布隆过滤器,第二个值是元素的个数
// 初始化一个存储string数据的布隆过滤器,初始化大小为100W
// 默认误判率是0.03
BloomFilter<String>

博客围绕布隆过滤器展开,介绍其用于解决缓存穿透问题。缓存穿透会使数据库崩溃,布隆过滤器利用位图和哈希函数判断元素是否存在,虽有一定误判率,但能在海量数据中快速判断。谷歌Guava提供了现成工具,还适用于爬虫、邮箱服务器等场景。
最低0.47元/天 解锁文章
1192





