一、布隆过滤器介绍
1、布隆过滤器的起源,用途
布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
2、布隆过滤器的概念
如果想要判断一个元素是不是在一个集合里,一般想到的是将所有元素保存起来,然后通过比较确定。链表,树等等数据结构都是这种思路. 但是随着集合中元素的增加,我们需要的存储空间越来越大,检索速度也越来越慢(O(n),O(logn))。不过世界上还有一种叫作散列表(又叫哈希表,Hash table)的数据结构。它可以通过一个Hash函数将一个元素映射成一个位阵列(Bit array)中的一个点。这样一来,我们只要看看这个点是不是1就可以知道集合中有没有它了。这就是布隆过滤器的基本思想。
Hash面临的问题就是冲突。假设Hash函数是良好的,如果我们的位阵列长度为m个点,那么如果我们想将冲突率降低到例如 1%, 这个散列表就只能容纳m / 100个元素。显然这就不叫空间效率了(Space-efficient)了。解决方法也简单,就是使用多个Hash,如果它们有一个说元素不在集合中,那肯定就不在。如果它们都说在,虽然也有一定可能性它们在说谎,不过直觉上判断这种事情的概率是比较低的。
3、布隆过滤器的优缺点
1、优点
相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数。另外, Hash函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。
布隆过滤器可以表示全集,其它任何数据结构都不能。
2、缺点
但是布隆过滤器的缺点和优点一样明显。误算率是其中之一。随着存入的元素数量增加,误算率随之增加。常见的补救办法是建立一个小的白名单,存储那些可能被误判的元素。但是如果元素数量太少,则使用散列表足矣。
另外,一般情况下不能从布隆过滤器中删除元素。我们很容易想到把位列阵变成整数数组,每插入一个元素相应的计数器加1, 这样删除元素时将计数器减掉就可以了。然而要保证安全的删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面. 这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。
在降低误算率方面,有不少工作,使得出现了很多布隆过滤器的变种。
4、应用场景
网页URL 去重
垃圾邮件识别
黑名单
查询加速【比如基于KV结构的数据】
集合元素重复的判断
缓存穿透
5、布隆过滤器的工作原理
布隆过滤器本身是一个很长的二进制向量,既然是二进制的向量,那么显而易见的,存放的不是0,就是1。
新建一个16位的布隆过滤器,如图
有一个对象,我们通过
- 方式一计算他的hash值,得到hash = 2
- 方式二计算他的hash值,得到hash = 9
- 方式三计算他的hash值,得到hash = 5
通过三个方法计算得到三个数值,我们把这三个数值对应的布隆过滤器向量值改为1,表明该位置有值。
第二个对象,加入得到值1 6 3,就把1 6 3 改为1
对于布隆过滤器本身来说,并没有存储任何数据,只是计算该数据的位置,然后存储向量值
那么,如果需要判断某个数据是否存在于布隆过滤器,就只需要判断计算出来的所有向量值是否都为1即可
但是:
当存储的数据向量不断增多,就可能会出现,2 9 5 向量值都为1,但是实际上没有这个数据的情况,这样就导致了,布隆过滤器只能判断某个数据一定不存在,但是不能保证某个数据一定存在。
另外,因为一个向量位置可能被多个对象映射,所以,布隆过滤器无法删除数据
6、布隆过滤器的设计
布隆过滤器思路比较简单,但是对于布隆过滤器的随机映射函数设计,需要计算几次,向量长度设置为多少比较合适,这个才是需要认真讨论的。
如果向量长度太短,会导致误判率直线上升。
如果向量太长,会浪费大量内存。
如果计算次数过多,会占用计算资源,且很容易很快就把过滤器填满。
二、Guava布隆过滤器的使用
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
//tddBloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 10000000, 0.01);
tddBloomFilter = BloomFilter.create(Funnels.longFunnel(), 10000000, 0.01);
toBloomFilter = BloomFilter.create(Funnels.longFunnel(), 10000000, 0.01);
tddBloomFilter.put(1L);
tddBloomFilter.put(2L);
tddBloomFilter.put(3L);
toBloomFilter.put(1L);
toBloomFilter.put(2L);
toBloomFilter.put(3L);
tddBloomFilter.mightContain(tddId);
tddBloomFilter.mightContain(1);
toBloomFilter.mightContain(toId);
toBloomFilter.mightContain(1);
完整代码
package com.cqsym.nbigscreen.bloomfilter;
import com.cqsym.nbigscreen.config.DataSourceRepository;
import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Component
public class NbigscreenBloom {
private static final Logger log = LoggerFactory.getLogger(NbigscreenBloom.class);
private static final int BATCH_SIZE = 10000; // 每次查询的数据量
private static final int BloomFilter_init_size = 1000000; // 初始化布隆过滤数据量,100万。
private static final int BloomFilter_size = BloomFilter_init_size * 2; // 布隆过滤数据量大小,200万。
public static BloomFilter tddBloomFilter;
public static BloomFilter toBloomFilter;
@Autowired
@Qualifier(value="secondDataSourceRepository")
private DataSourceRepository secondDataSourceRepository;
public void initForDay() {
//tddBloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 10000000, 0.01);
tddBloomFilter = BloomFilter.create(Funnels.longFunnel(), BloomFilter_size, 0.01);
toBloomFilter = BloomFilter.create(Funnels.longFunnel(), BloomFilter_size, 0.01);
try {
log.info("initForDay ---------- 睡5秒 开始 ... ");
TimeUnit.SECONDS.sleep(5);
log.info("initForDay ---------- 睡5秒 完成。 ");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("initForDay ---------- start initForTdd() ... ");
new Thread(() -> {
log.info("initForDay1 ---------- start initForTdd() ... ");
initForTdd();
log.info("initForDay1 ---------- end initForTdd(). ");
},"initForTdd线程").start();
log.info("initForDay ---------- end initForTdd(). ");
log.info("initForDay ---------- start initForTo() ... ");
new Thread(() -> {
log.info("initForDay1 ---------- start initForTo() ... ");
initForTo();
log.info("initForDay1 ---------- end initForTo(). ");
},"initForTo线程").start();
log.info("initForDay ---------- end initForTo(). ");
}
public void initForTdd() {
final String sql0 = "select count(1) from t_dispatch_detail ";
final String sql = "select tdd_id from t_dispatch_detail limit ? , ? ";
//int offset = 0;
//int offset = 20000000;
int offset = secondDataSourceRepository.queryForListSingleColumn(sql0, Integer.class).get(0)-BloomFilter_init_size;
log.info("initForTdd ---------- First offset: " + offset);
List<Long> rows;
do {
log.info("initForTdd ---------- offset: " + offset);
rows = secondDataSourceRepository.queryForListSingleColumn(sql, Long.class, offset, BATCH_SIZE);
for (Long row : rows) {
tddBloomFilter.put(row);
}
offset += BATCH_SIZE;
} while (rows.size() == BATCH_SIZE);
log.info("initForTdd ---------- end");
}
public void initForTo() {
final String sql0 = "select count(1) from t_order ";
final String sql = "select to_id from t_order limit ? , ? ";
//int offset = 0;
//int offset = 12000000;
int offset = secondDataSourceRepository.queryForListSingleColumn(sql0, Integer.class).get(0)-BloomFilter_init_size;
log.info("initForTo ---------- First offset: " + offset);
List<Long> rows;
do {
log.info("initForTo ---------- offset: " + offset);
rows = secondDataSourceRepository.queryForListSingleColumn(sql, Long.class, offset, BATCH_SIZE);
for (Long row : rows) {
toBloomFilter.put(row);
}
offset += BATCH_SIZE;
} while (rows.size() == BATCH_SIZE);
log.info("initForTo ---------- end");
}
}
package com.fandf.test.redis;
import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
/**
* Guava
*/
public class GuavaBloomFilter {
public static void main(String[] args) {
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),100000,0.01);
bloomFilter.put("好好学技术");
System.out.println(bloomFilter.mightContain("不好好学技术"));
System.out.println(bloomFilter.mightContain("好好学技术"));
}
}
三、Redis布隆过滤器的使用
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.20.0</version>
</dependency>
package com.fandf.test.redis;
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
/**
* Redisson 实现布隆过滤器
*/
public class RedissonBloomFilter {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
//构造Redisson
RedissonClient redisson = Redisson.create(config);
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("name");
//初始化布隆过滤器:预计元素为100000000L,误差率为1%
bloomFilter.tryInit(100000000L,0.01);
bloomFilter.add("好好学技术");
System.out.println(bloomFilter.contains("不好好学技术"));
System.out.println(bloomFilter.contains("好好学技术"));
}
}
四、hutool布隆过滤器的使用
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.3</version>
</dependency>
package com.fandf.test.redis;
import cn.hutool.bloomfilter.BitMapBloomFilter;
import cn.hutool.bloomfilter.BloomFilterUtil;
/**
* hutool
*/
public class HutoolBloomFilter {
public static void main(String[] args) {
BitMapBloomFilter bloomFilter = BloomFilterUtil.createBitMap(1000);
bloomFilter.add("好好学技术");
System.out.println(bloomFilter.contains("不好好学技术"));
System.out.println(bloomFilter.contains("好好学技术"));
}
}
五、java自定义实现布隆过滤器
package com.fandf.test.redis;
import java.util.BitSet;
/**
* java布隆过滤器
*/
public class MyBloomFilter {
/**
* 位数组大小
*/
private static final int DEFAULT_SIZE = 2 << 24;
/**
* 通过这个数组创建多个Hash函数
*/
private static final int[] SEEDS = new int[]{4, 8, 16, 32, 64, 128, 256};
/**
* 初始化位数组,数组中的元素只能是 0 或者 1
*/
private final BitSet bits = new BitSet(DEFAULT_SIZE);
/**
* Hash函数数组
*/
private final MyHash[] myHashes = new MyHash[SEEDS.length];
/**
* 初始化多个包含 Hash 函数的类数组,每个类中的 Hash 函数都不一样
*/
public MyBloomFilter() {
// 初始化多个不同的 Hash 函数
for (int i = 0; i < SEEDS.length; i++) {
myHashes[i] = new MyHash(DEFAULT_SIZE, SEEDS[i]);
}
}
/**
* 添加元素到位数组
*/
public void add(Object value) {
for (MyHash myHash : myHashes) {
bits.set(myHash.hash(value), true);
}
}
/**
* 判断指定元素是否存在于位数组
*/
public boolean contains(Object value) {
boolean result = true;
for (MyHash myHash : myHashes) {
result = result && bits.get(myHash.hash(value));
}
return result;
}
/**
* 自定义 Hash 函数
*/
private class MyHash {
private int cap;
private int seed;
MyHash(int cap, int seed) {
this.cap = cap;
this.seed = seed;
}
/**
* 计算 Hash 值
*/
int hash(Object obj) {
return (obj == null) ? 0 : Math.abs(seed * (cap - 1) & (obj.hashCode() ^ (obj.hashCode() >>> 16)));
}
}
public static void main(String[] args) {
String str = "好好学技术";
MyBloomFilter myBloomFilter = new MyBloomFilter();
System.out.println("str是否存在:" + myBloomFilter.contains(str));
myBloomFilter.add(str);
System.out.println("str是否存在:" + myBloomFilter.contains(str));
}
}
import java.util.BitSet;
public class MyBloomFilter {
// 默认大小
private static final int DEFAULT_SIZE = Integer.MAX_VALUE;
// 最小的大小
private static final int MIN_SIZE = 1000;
// 大小为默认大小
private int SIZE = DEFAULT_SIZE;
// hash函数的种子因子
private static final int[] HASH_SEEDS = new int[]{3, 5, 7, 11, 13, 17, 19, 23, 29, 31};
// 位数组,0/1,表示特征
private BitSet bitSet = null;
// hash函数
private HashFunction[] hashFunctions = new HashFunction[HASH_SEEDS.length];
// 无参数初始化
public MyBloomFilter() {
// 按照默认大小
init();
}
// 带参数初始化
public MyBloomFilter(int size) {
// 大小初始化小于最小的大小
if (size >= MIN_SIZE) {
SIZE = size;
}
init();
}
private void init() {
// 初始化位大小
bitSet = new BitSet(SIZE);
// 初始化hash函数
for (int i = 0; i < HASH_SEEDS.length; i++) {
hashFunctions[i] = new HashFunction(SIZE, HASH_SEEDS[i]);
}
}
// 添加元素,相当于把元素的特征添加到位数组
public void add(Object value) {
for (HashFunction f : hashFunctions) {
// 将hash计算出来的位置为true
bitSet.set(f.hash(value), true);
}
}
// 判断元素的特征是否存在于位数组
public boolean contains(Object value) {
boolean result = true;
for (HashFunction f : hashFunctions) {
result = result && bitSet.get(f.hash(value));
// hash函数只要有一个计算出为false,则直接返回
if (!result) {
return result;
}
}
return result;
}
// hash函数
public static class HashFunction {
// 位数组大小
private int size;
// hash种子
private int seed;
public HashFunction(int size, int seed) {
this.size = size;
this.seed = seed;
}
// hash函数
public int hash(Object value) {
if (value == null) {
return 0;
} else {
// hash值
int hash1 = value.hashCode();
// 高位的hash值
int hash2 = hash1 >>> 16;
// 合并hash值(相当于把高低位的特征结合)
int combine = hash1 ^ hash1;
// 相乘再取余
return Math.abs(combine * seed) % size;
}
}
}
public static void main(String[] args) {
Integer num1 = new Integer(12321);
Integer num2 = new Integer(12345);
MyBloomFilter myBloomFilter =new MyBloomFilter();
System.out.println(myBloomFilter.contains(num1));
System.out.println(myBloomFilter.contains(num2));
myBloomFilter.add(num1);
myBloomFilter.add(num2);
System.out.println(myBloomFilter.contains(num1));
System.out.println(myBloomFilter.contains(num2));
}
}
package Bloomfilter;
import java.io.*;
import java.util.BitSet;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Created by Intellij IDEA.
* User: LYX
* Date: 2023/6/24
*/
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;
}
}
三种手写的代码,自己手写可以借鉴。