一、认识:
布隆过滤器,英文叫BloomFilter,可以说是一个二进制向量和一系列随机映射函数实现。 可以用于检索一个元素是否在一个集合中。
二、原理:
数据存储示意图-->
说明:bit数组中以n个索引位(n个不同的hash算法后的产生的索引)表示一个数据
小结:
- 布隆过滤器是用于判断一个元素是否在集合中。通过一个位数组和N个hash函数实现。
- 注意:布隆过滤器有宁可错杀一百,也不能放过一个的特质,只会误报,不会漏报
- 优点:
- 空间效率高,所占空间小。
- 查询时间短。
- 缺点:
- 元素添加到集合中后,不能被删除。
- 有一定的误判率
三、使用场景:
- 网页爬虫对URL的去重,避免爬去相同的URL地址。
- 垃圾邮件过滤,从数十亿个垃圾邮件列表中判断某邮箱是否是杀垃圾邮箱。
- 解决数据库缓存击穿,黑客攻击服务器时,会构建大量不存在于缓存中的key向服务器发起请求,在数据量足够大的时候,频繁的数据库查询会导致挂机。
- 秒杀系统,查看用户是否重复购买
四、实现方式:
根据布隆过滤器的原理,其实各种语言都有相应的实现,比如java、python、scala、redis、lua等等,各自有各自的优化,目的都是为了降低误报率和空间利用率。
1)Java实现
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.BitSet;
public class CustomBloomFilter {
//数据个数
private int size;
//bit[]长度
private int bitSize;
//bit[]容器
private BitSet bitMap;
//hash函数个数
private int hashNum;
//误判率
private double cap;
/**
*
* @param size 数据量
* @param cap 误判率
*/
public CustomBloomFilter(int size, double cap) {
this.size = size;
this.cap = cap;
}
/**
* 初始化数据至容器
*/
public synchronized void init(){
if(this.bitSize == 0){
this.bitSize = (int)((-size * Math.log(this.cap)) / (Math.pow(Math.log(2),2)));
}
if(this.hashNum == 0){
this.hashNum = Math.max(1,(int)Math.round(this.bitSize / this.size * Math.log(2)));
}
if(this.bitMap == null) {
bitMap = new BitSet(this.bitSize);
}
System.out.println("this.bitSize :" + this.bitSize);
System.out.println("this.hashNum :" + this.hashNum);
}
/**
* 添加元素至容器中
* @param ele
*/
public void add(String ele){
if(bitMap == null){
init();
}
int[] posArr = getIndexs(ele);
for (int t : posArr) {
bitMap.set(t,true);
}
}
/**
* 判断元素是否存在
* @param ele 元素
* @return
*/
public boolean isExist(String ele){
int[] posArr = getIndexs(ele);
boolean flag = true;
for (int t : posArr) {
flag = flag && bitMap.get(t);
}
return flag;
}
/**
* 获取元素计算得出的下标集
* @param ele 元素
* @return
*/
public int[] getIndexs(String ele){
int[] retArr = new int[this.hashNum];
for (int i = 0; i < this.hashNum; i++) {
retArr[i] = HashUtil.md5Hash(ele + i) % this.bitSize;//运用算法得出下标
}
return retArr;
}
public static class HashUtil{
public static int md5Hash(String ele){
try {
// 生成一个MD5加密计算摘要
MessageDigest md = MessageDigest.getInstance("MD5");
// 计算md5函数
md.update(ele.getBytes());
// digest()最后确定返回md5 hash值,返回值为8位字符串。因为md5 hash值是16位的hex值,实际上就是8位的字符
// BigInteger函数则将8位的字符串转换成16位hex值,用字符串来表示;得到字符串形式的hash值
//一个byte是八位二进制,也就是2位十六进制字符(2的8次方等于16的2次方)
String s = new BigInteger(1, md.digest()).toString(16);
return Arrays.hashCode(s.getBytes()) & Integer.MAX_VALUE;
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
}
public static void main(String[] args) {
CustomBloomFilter bloomFilter = new CustomBloomFilter(10000,0.003);
for (int i = 0; i < 10000; i++) {
bloomFilter.add("abc" + i);
}
int count = 0;
for (int i = 0; i < 20000; i++) {
if(bloomFilter.isExist("abc" + i)){
count++;
}
}
System.out.println("count:" + count);
}
}
2)Redis实现
RedisBloom模块不仅仅实现了布隆过滤器,还实现了 CuckooFilter(布谷鸟过滤器),以及 TopK 功能。CuckooFilter 是在 BloomFilter 的基础上主要解决了BloomFilter不能删除的缺点。
#安装RedisBloom模块
git clone https://github.com/RedisBloom/RedisBloom.git
cd RedisBloom
make #编译 会生成一个rebloom.so文件
redis-server --loadmodule /path/to/rebloom.so
redis-cli -h 127.0.0.1 -p 6379
复制代码
127.0.0.1:8001> bf.reserve bloom_filter_test 0.0000001 1000000
OK
127.0.0.1:8001> bf.reserve bloom_filter_test 0.0000001 1000000
(error) ERR item exists
127.0.0.1:8001>
127.0.0.1:8001>
127.0.0.1:8001> bf.add bloom_filter_test key1
(integer) 1
127.0.0.1:8001> bf.add bloom_filter_test key2
(integer) 1
127.0.0.1:8001>
127.0.0.1:8001> bf.madd bloom_filter_test key2 key3 key4 key5
1) (integer) 0
2) (integer) 1
3) (integer) 1
4) (integer) 1
127.0.0.1:8001> bf.exists bloom_filter_test key2
(integer) 1
127.0.0.1:8001> bf.exists bloom_filter_test key3
(integer) 1
127.0.0.1:8001> bf.mexists bloom_filter_test key3 key4 key5
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:8001>
注释:
- bloom filter定义
BF.RESERVE {key} {error_rate} {capacity}
使用给定的期望错误率和初始容量创建空的Bloom过滤器(如果不存在的话)。如果打算向Bloom过滤器中添加许多项,则此命令非常有用,否则只能使用BF.ADD 添加项。
初始容量和错误率将决定过滤器的性能和内存使用情况。一般来说,错误率越小(即对误差的容忍度越低),每个过滤器条目的空间消耗就越大。
- BF.ADD {key} {item}
单条添加元素
向Bloom filter添加一个元素,如果该key不存在,则创建该key(过滤器)。如果项是新插入的,则为“1”;如果项以前可能存在,则为“0”。
- BF.MADD {key} {item} [item...]
批量添加元素
布尔数(整数)的数组。返回值为0或1的范围的数据,这取决于是否将相应的输入元素新添加到过滤器中,或者是否已经存在。
- BF.EXISTS {key} {item}
判断单个元素是否存在
如果存在,返回1,否则返回0
- BF.MEXISTS {key} {item} [item...]
判断多个元素是否存在
布尔数(整数)的数组。返回值为0或1的范围的数据,这取决于是否将相应的元是否已经存在于key中。
- bf.insert
bf.insert{key} [CAPACITY {cap}] [ERROR {ERROR}] [NOCREATE] ITEMS {item…}
该命令将向bloom过滤器添加一个或多个项,如果它还不存在,则默认情况下创建它。有几个参数可用于修改此行为。
key:过滤器的名称
capacity:如果指定了,应该在后面加上要创建的过滤器的所需容量。如果过滤器已经存在,则忽略此参数。如果自动创建了过滤器,并且没有此参数,则使用默认容量(在模块级指定)。见bf.reserve。
error:如果指定了,后面应该跟随着新创建的过滤器的错误率(如果它还不存在)。如果自动创建过滤器而没有指定错误,则使用默认的模块级错误率。见bf.reserve。
nocreate:如果指定,表示如果过滤器不存在,就不应该创建它。如果过滤器还不存在,则返回一个错误,而不是自动创建它。如果需要在创建过滤器和添加过滤器之间进行严格的分离,可以使用这种方法。将NOCREATE与容量或错误一起指定是一个错误。
item:指示要添加到筛选器的项的开头。必须指定此参数。
- bf持久化操作
BF.SCANDUMP {key} {iter}
对bloom过滤器进行增量保存。这对于不能适应常规save和restore模型的大型bloom filter非常有用。
第一次调用这个命令时,iter的值应该是0。这个命令将返回连续的(iter, data)对,直到(0,NULL),以表示完成
- blool filter数据类型的属性
bf.debug
127.0.0.1:8001> bf.debug bloom_filter_test
1) "size:5"
2) "bytes:4194304 bits:33554432 hashes:24 hashwidth:64 capacity:1000200 size:5 ratio:1e-07"
3)Google guava实现:
对于Google布隆过滤器来说,在不做任何设置的情况下,默认的误判率为0.03
引入jar包
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>22.0</version>
</dependency>
测试
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 10000,0.003);
for (int i = 0; i < 10000; i++) {
bloomFilter.put("abc" + i);
}
int count = 0;
for (int i = 0; i < 20000; i++) {
if(bloomFilter.mightContain("abc" + i)){
count++;
}
}
System.out.println("count:" + count);
拓展:
该文章 对比了布隆过滤器和布谷鸟过滤器,相比布谷鸟过滤器,布隆过滤器有以下不足:
- 查询性能弱 查询性能弱是因为布隆过滤器需要使用N个 hash函数计算位数组中N个不同的点,这些点在内存上跨度较大,会导致CPU缓存命中率低。
- 空间利用效率低 在相同的误判率下,布谷鸟的空间利用率要高于布隆过滤器,能节省40%左右。
- 不支持删除操作 这是布谷鸟过滤器相对布隆过滤器最大的优化,支持反向操作,删除功能。
- 不支持计数
五、公式推导: