在聊布隆过滤器之前先说一下某些场景,比如:
1.我们为了减少数据库的压力经常用到redis缓存,当请求过来时会先查询是否能命中缓存,缓存不存在的时候再去访问数据库,如果这时候有人想攻击我们的系统,构建了很多数据库中不存在的数据,那么这些大量的请求不会命中缓存而是直接到数据库中,也就是缓存穿透,严重会造成数据库崩溃,服务宕机等。
2.还有一个场景,我们需要从海量数据中判断一些数据是否存在,常规查数据到内存中进行筛选的话很容易造成内存溢出,效率低下等问题。
针对上面两个场景,有个很好的解决方案,就是设计一个过滤器能判断数据是否存在其中,不存在的话就直接过滤掉。
布隆过滤器:先说一下布隆过滤器的原理就是一个位数组,默认值都是0,当数据进来的时候通过不同的hash算法计算hash值,将对应的值设置为1,当再有数据进来进行相同的hash算法计算,如果计算的位数组的数据都是1,则认为这个数据可能存在,如果有一个不是1则说明这个数据一定不存在。如下图所示:
经过上面的了解,是不是能解决我们最开始提出的问题了,我们可以将数据库的数据都保存到布隆过滤器中,当请求来的时候,先判断是否存在,不存在的话就就直接返回,如果判断存在的话再去请求redis,redis也不存在的话才去查询数据库。
缺点:虽然布隆过滤器很好用,但是也有一定的缺点,比如
1.判断结果不准确,有可能会误判,判断数据存在(数据可能不存在),但是判断数据不存在的话就一定没有
2.数据准确性会随着放入数据的增加逐渐降低
3.存入元素无法删除
实战:
1.手写
package com.example.demo.util;
import java.util.BitSet;
import java.util.Random;
/**
● 布隆过滤器
*/
public class BloomFilter {
/**
○ 位数组,用于存储布隆过滤器的状态
*/
private BitSet bitSet;
/**
○ 位数组的长度
*/
private int bitSetSize;
/**
○ 预期元素数量
*/
private int expectedNumberOfElements;
/**
○ 哈希函数数量
*/
private int numberOfHashFunctions;
/**
○ 用于生成哈希种子的伪随机数生成器
*/
private Random random = new Random();
public BloomFilter(int bitSetSize, int expectedNumberOfElements) {
this.bitSetSize = bitSetSize;
this.expectedNumberOfElements = expectedNumberOfElements;
// 根据公式计算哈希函数数量
this.numberOfHashFunctions = (int) Math.round((bitSetSize / expectedNumberOfElements) * Math.log(2.0));
// 创建位数组并初始化所有位为0
this.bitSet = new BitSet(bitSetSize);
}
public void add(Object element) {
// 对元素进行多次哈希,并将对应的位设置为1
for (int i = 0; i < numberOfHashFunctions; i++) {
long hash = computeHash(element.toString(), i);
int index = getIndex(hash);
bitSet.set(index, true);
}
}
public boolean contains(Object element) {
// 对元素进行多次哈希,并检查所有哈希值所对应的位是否都被设置为1
for (int i = 0; i < numberOfHashFunctions; i++) {
long hash = computeHash(element.toString(), i);
int index = getIndex(hash);
if (!bitSet.get(index)) {
return false;
}
}
return true;
}
private int getIndex(long hash) {
// 将哈希值映射到位数组的下标(需要确保下标非负)
return Math.abs((int) (hash % bitSetSize));
}
private long computeHash(String element, int seed) {
// 使用伪随机数生成器生成不同的哈希种子
random.setSeed(seed);
// 将元素转换为字节数组,并计算其哈希值
byte[] data = element.getBytes();
long hash = 0x7f52bed27117b5efL;
for (byte b : data) {
hash ^= random.nextInt();
hash *= 0xcbf29ce484222325L;
hash ^= b;
}
return hash;
}
}
2.测试
public static void main(String[] args) {
List strings = Arrays.asList("数学", "语文", "历史", "政治", "地理","生物");
BloomFilter filter = new BloomFilter(10000, 3);
// 将所有字符串添加到布隆过滤器中
for (String s : strings) {
filter.add(s);
}
String[] queries = {"数学", "语文", "生物", "java"};
for (String query : queries) {
System.out.println("是否包含:" + query + "-" + filter.contains(query));
}
}
现在网上都有封装好的工具如:Google Guava 提供的 BloomFilter 类来实现布隆过滤器,引入对应依赖即可使用。
布谷鸟过滤器
起源:布谷鸟算法的启发来自于布谷鸟,类似鸠占鹊巢
原理:布谷鸟过滤器由一个数组组成,数组中每个元素大小为4个字节,可以存储4个指纹,每个指纹占一个字节(2^8 = 256种)。这样有以下两个好处:
1.避免出现hash后的位置一致而导致的循环挤兑的情况。这样即使两个元素被 hash 在了同一个位置,也不必立即「鸠占鹊巢」,因为这里有4个座位,你可以随意坐一个。除非这多个座位都被占了,才需要进行挤兑。这会显著降低挤兑次数。
2.同一个位置上的多个座位在内存空间上是连续的,可以有效利用 CPU 高速缓存。
插入:
初始化一个给定容量的过滤器Filter,根据插入值先进行一次hash,得出应当插入位置和应当插入的值。如果这个这个位置(bucket内的4个位置均被占用)插入失败,会进行第二次hash,查看第二个位置能否插入。若第二个位置插入失败,则会随机在两个位置挑选一个将其中的一个值标记为旧值,用新值覆盖旧值,旧值会在重复上面的步骤进行插入。
会对插入的值进行校验,只有当未插入过该值时才会插入成功,若过滤器中已经存在该值,会插入失败返回false。
扩容:
如果数组过小,会发生循环挤兑的情况,如果超过最大挤兑次数,进行扩容,重新计算每个指纹的位置。
查找:
用两个hash函数计算,将计算结果与两个元素中的8个位置的指纹进行对比,如果对比成功则表示数据存在。
删除
通过两次hash找到索引位置,若有该数据,将该位置数据删除。因为每个对象的指纹会存储到一个位置中,所以可以通过删除这个指纹来删除数据。布隆过滤器无法删除元素。
优点:
1.支持新增和删除元素。
布隆过滤器不支持删除元素
2.更节省空间。
布谷鸟过滤器在错误率小于3%的时候空间性能是优于布隆过滤器
布谷鸟过滤器比布隆过滤器空间节省40%多
3.查询效率很高
布谷鸟过滤器只需一次哈希
布隆过滤器要采用多种哈希函数进行多次哈希
缺点:
1.插入性能较差
布谷鸟过滤器在计算哈希后可能当前位置上已经存储了指纹,这时就要将已存储的项踢到候选桶,随着桶越来越满,产生冲突的可能性越来越大,插入耗时越来越高
布隆过滤器插入时计算好哈希直接写入位即可
2.插入重复元素存在上限
布谷鸟过滤器对已存在的值会做踢出操作,因此重复元素的插入存在上限。
布隆过滤器在插入重复元素时并没有影响,只是对已存在的位再置一遍。
3.空间大小
布谷鸟过滤器必须是2的指数。
布隆过滤器不需要2的指数。