前言
对于缓存穿透问题,布隆过滤器就是一种很好的解决方案。缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库,失去了缓存的意义。简而言之就是缓存中不存在,然后到数据库查询也不存在。当然,也可以设置黑白名单、分布式锁等其它方式解决,此处只讲述布隆过滤器,其它方案友友们可以自行研究。
概念
布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
原理
首先布隆过滤器是一个bit数组,如图所示:
例如我们把华为这个信息映射到布隆过滤器中,使用三个不同的哈希函数映射到对应的bit数组的位置上(假设计算出来的位置为3,5,7),使其映射到的位置上的0变为1,如图所示:
再将苹果的信息也按照相同的方式映射到bit数组上(假设计算出来的位置为1,5,6),如图所示:
很显然,华为信息映射到的5的位置被苹果信息映射到的5的位置替换了,那么就明显可以说明如果布隆过滤器上没有某个信息,那么经过哈希处理后bit数组位置上的值应该全部为0,如果为1,该信息有可能存在也有可能不存在
,就像上面的苹果信息计算出来的位置把原有位置的信息覆盖掉了。因此布隆过滤器上有映射,但实际数据也不一定存在。
布隆过滤器参数关系
根据上面的情况而言,很显然,长度过小的布隆过滤器很快所有的bit位都被置为1了,查询到的任意值都会返回“可能存在”的,这样的话那么布隆过滤器就失去了意义。说明,布隆过滤器的长度越小,其误报率就越高,布隆过滤器的长度越长,误报率越低。
对于不当的哈希函数的个数也会对对误报率有影响。例如哈希函数的个数越多,数组的bit位会被迅速填满,会加快布隆过滤器bit位置为1的速度,那么布隆过滤器的效率就会越低。简单来说,如果哈希函数的个数越多,布隆过滤器bit位置为1的速度就越快,且效率就会越低;如果哈希函数个数越少,bit位置为1的速度就越慢,但是误报率就会明显变高了。因此,布隆过滤器的长度、哈希函数的个数、误报率以及插入元素的个数这几者之间存在关系。该关系如下所示,在这里小编不对公式做过多的赘述,有兴趣的学者可以自行参考下面详情。
- 设bitarray大小为m,样本数量为n,失误率为p
- 单个样本大小不影响布隆过滤器大小,因为样本会通过哈希函数得到输出值
- 使用样本数量n和失误率p可以算出m,公式为:
- 所使用哈希函数个数k可以由以下公式求得:
- 可以通过以下公式算出设计的布隆过滤器的真实失误率:
公式推导如下:
具体可以点->这里<-参考
优缺点分析
优点
- 相比于传统的List、Set、Map等数据结构,它占用空间更少,因为其本身并不存储任何数据
- 时间复杂度低,增加和查询元素的时间复杂为O(N),(N为哈希函数的个数,通常情况比较小)
- 保密性强,布隆过滤器不存储元素本身
缺点
- 其返回的结果是概率性(存在误差)的
- 很难实现删除操作
- 无法获取元素本身
使用场景
1、网页爬虫对URL的去重,避免爬去相同的URL地址
2、垃圾邮件过滤,从数十亿个垃圾邮件列表中判断某邮箱是否是垃圾邮箱
3、解决数据库缓存击穿,黑客攻击服务器时,会构建大量不存在于缓存中的key向服务器发起请求,在数据量足够大的时候,频繁的数据库查询会导致挂机
4、秒杀系统,查看用户是否重复购买
编码实现
有很多业界的牛人大咖已经实现了布隆过滤器,例如谷歌的guava以及hutool都已经进行了实现,此处小编仅仅用简单的java方式编码实现,也方便大家更容易理解,并没有做过多的繁琐处理。
package com.ssy.www.mode;
import java.util.BitSet;
import java.util.Random;
/**
* @author ssy
* @className BloomFilter
* @description 简单布隆过滤器的实现
* @date 2023-11-18 09:56:31
*/
public class BloomFilter {
private BitSet bitmap;
private Integer bitmapSize;
private Integer hashFunctionCount;
private Integer insertElementCount;
private Random random = new Random();
public BloomFilter(Integer bitmapSize, Integer insertElementCount){
if(bitmapSize<0){
throw new IllegalArgumentException("布隆过滤器长度不能小于0");
}
if(insertElementCount<0){
throw new IllegalArgumentException("插入的元素个数不能小于0");
}
if(bitmapSize <= insertElementCount){
throw new IllegalArgumentException("布隆过滤器长度必须大于插入的元素个数");
}
this.bitmapSize = bitmapSize;
this.insertElementCount = insertElementCount;
hashFunctionCount = (int) Math.round(bitmapSize/insertElementCount * Math.log(2.0));
this.bitmap = new BitSet(this.bitmapSize);
}
public void add(Object obj){
if(obj==null){
return;
} else {
String str = obj.toString();
for (Integer i = 0; i < hashFunctionCount; i++) {
long hash = hash(str, i);
int index = getIndex(hash);
bitmap.set(index,Boolean.TRUE);
}
}
}
public boolean contains(Object obj){
if(obj==null) return false;
String str = obj.toString();
for (Integer i = 0; i < hashFunctionCount; i++) {
long hash = hash(str, i);
int index = getIndex(hash);
if (!bitmap.get(index)) {
return false;
}
}
return true;
}
private long hash(String element, int seed){
random.setSeed(seed);
long hash = 0x520abc1314defL;
byte[] bytes = element.getBytes();
for (int i = 0; i < bytes.length; i++) {
hash ^= random.nextInt();
hash *= 0x8023aa2157ffL;
hash ^= bytes[i];
}
return hash;
}
private int getIndex(long hash){
int index = Math.abs( (int)(hash % bitmapSize));
return index;
}
}
当然,上述的代码亦可以通过设置泛型,为后续指定计算具体类型的bit位置,小编在此仅做简单演示。