1970年,布隆提出布隆过滤器(BloomFilter)
,用于判断一个元素是否不在一个集合中
,但是不能精确确定元素在集合中。
通常情况下,要确定一个元素是否存在于某个业务场景的集合,一般是将元素保存到集合中,然后通过比较确定,例如采用链表、树、散列表(又叫哈希表,Hash Table)等数据结构。但是,随着集合中元素的增加,需要的存储空间也会呈现线性增长,最终达到瓶颈,同时检索速度也越来越慢。因此,为了解决这种时间和空间性能问题,引入了布隆过滤器。
1. 数据结构与原理
BloomFilter是由一个固定大小的二进制向量
或者位图(Bitmap),和一系列映射函数组成。
1.1 初始化
初始状态时,需要指定位图(数组)长度(N),并将所有位都都置为0
。
上图所示,假设N=18
,则初始化Array=00 0000 0000 0000 0000
。
1.2 变量映射
布隆过滤器映射需要指定映射次数K,即变量需要映射到位图的K个点上,并将其位置值设置为1。
上图所示,假设位图长度N=18
,映射次数K=3
,哈希计算函数分别为hash1
,hash2
,hash3
。
那么,对于元素A={x, y, z}
加入布隆过滤器,则:
元素 | 第一次映射位置 | 第二次映射位置 | 第三次映射位置 |
---|---|---|---|
x | hash1(x) % N = 1 | hash2(x) % N = 5 | hash3(x) % N = 13 |
y | hash1(y) % N = 4 | hash2(y) % N = 11 | hash3(y) % N = 16 |
z | hash1(z) % N = 3 | hash2(z) % N = 5 | hash3(z) % N = 11 |
最终,得到布隆过滤器串为:01 0111 0000 0101 0010
1.3 变量检索
此时,元素w
加入,需要判断是否不存在。首先,经过K次映射计算其所在布隆过滤器位置。
元素 | 第一次映射位置 | 第二次映射位置 | 第三次映射位置 |
---|---|---|---|
w | hash1(x) % N = 4 | hash2(x) % N = 13 | hash3(x) % N = 15 |
其次,读取映射位置所在元素值,可以发现第三次映射位置
所在值为0,那么判定元素w
不存在于过滤器中。
1.4 总结
某个变量检索时,只要判断映射点值是否都为1,可以大概率判断是否存在于集合中。
- 如果映射点有任何一个0,则被检索变量一定不存在;
- 如果都是1,则被检索变量很可能存在。
思考:为什么说是可能存在,而不是一定存在呢?那是因为映射函数本身就是散列函数,散列函数是会有碰撞的。
2. 过滤器特性
2.1 误判率
布隆过滤器误判
:多个键经过映射后在相同bit位设置为1,导致无法判断究竟是哪个输入产生的,因此误判的根源在于相同bi 位被多次映射且设置为1。
这种情况也造成了布隆过滤器的删除存在问题,因为布隆过滤器每个bit并不是独占的,很有可能多个元素共享了某一位。如果我们直接删除这一位的话,会影响其他的元素。
2.2 判断特点
- 一个元素如果判断结果为存在的时候元素不一定存在,但是判断结果为不存在的时候则一定不存在。
- 布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加。
3. 案列代码
package org.apache.ithink;
/**
* bloom filter
* @author ithink/guojl.szu@hotmail.com
* @date 2021-09-04
*/
public class BloomFilter {
/**
* 映射次数
*/
private int k;
/**
* 每个Key占用字节数
*/
private int bitsPerKey;
/**
* 位图大小
*/
private int bitSize;
/**
* 底层存储位图
*/
private byte[] array;
public BloomFilter(int k, int bitsPerKey) {
this.k = k;
this.bitsPerKey = bitsPerKey;
}
/**
* 生成键值的位图
* @param keys 键
* @return 键对应位图
*/
public byte[] generate(String[] keys) {
// 计算所有Key占用位数
int actualBitSize = keys.length * bitsPerKey;
// 由于每个字节占用8位
// bitSize + 7 / 8: 避免bitSize < 8, 导致bitSize / 8为0
// ((bitSize + 7) / 8) << 3: 保证位数是8的整数倍
int middleBitSize = ((actualBitSize + 7) / 8) << 3;
// Key最少占用64位
int bitSize = middleBitSize < 64 ? 64 : middleBitSize;
// 创建位图, 因为byte是8位, 所以需要将所有位数除以8
array = new byte[bitSize >> 3];
// 保存位图占用大小
this.bitSize = bitSize;
// 循环处理
for (int i = 0; i < keys.length; i++) {
// 生成Hash值
int hashCode = Bytes.hash(keys[i].getBytes());
// 循环映射K次
for (int t = 0; t < k; t++) {
// 计算所在位图位置
// hashCode % bitSize + bitSize: 保证取模之后不为负数
// (hashCode % bitSize + bitSize) % bitSize: 有可能括号内超过bitSize,
// 所以需要再次取模计算
int position = (hashCode % bitSize + bitSize) % bitSize;
// 由于每个字节占用8位
// position % 8: 计算位置所在当前字节第几位
// (1 << (position % 8)): 将所在字节位值设置为1
// position / 8: 计算position所在字节位置
array[position / 8] |= (1 << (position % 8));
// 将hashCode前17位和后15位进行位置切换
int delta = (hashCode >> 17) | (hashCode << 15);
// 保证每次计算的hashCode不同
hashCode += delta;
}
}
return array;
}
/**
* 判断Key是否存在
* @param key 键
* @return true:存在, false:不存在
*/
public boolean contains(String key) {
// 生成哈希值
int hashCode = Bytes.hash(key.getBytes());
// 循环映射K次
for (int t = 0; t < k; t++) {
// 计算所在位图位置
int position = (hashCode % bitSize + bitSize) % bitSize;
// 如果只要一个位置值为0, 则表示不存在
if ((array[position / 8] & (1 << (position % 8))) == 0) {
return false;
}
// 将hashCode前17位和后15位进行位置切换
int delta = (hashCode >> 17) | (hashCode << 15);
hashCode += delta;
}
return true;
}
}