这是我参与8月更文挑战的第N天,活动详情查看:8月更文挑战
布隆过滤器的主要应用场景是判断一个元素是否在一大数据量集合中。
从熟悉的Hash冲突说起
java中屏蔽了物理地址的概念,也就是对象实际存储在内存的地址,交给了JVM
hashcode : 根据对象的某些信息推导出的一个整数值,用来表示逻辑地址
那么hashcode既然是根据一定的算法算出来的,那就一定存在两个不同的对象hashcode是相同的。
上面即为两组hash冲突的案例,大家可以试一下,在线调试地址
布隆过滤器前传
假设我们有一个几十亿级
别规模的电话名单,如何判断一个电话号码是否在名单中呢?
传统数据库
: 几十亿级别的数据,在查询时会变得慢速。
放在内存中
: 即使用Java中set以及redis中的基本数据类型。数据量过大,占用空间达到几十G。
可实现的方法
bitmap解决问题
优点: bitmap使用512M内存就可以存储多大42.9亿的字节信息。 因为存的是电话号码。那么我们可以将其存在redis的bitmap中,电话号码为偏移量。
缺点: Bitmap支持的最大位数是2^32位。
hyperloglog解决问题
优点: hyperloglog并不会存储数据,最对只占用12KB。可以用pfadd 命令的返回值判断是否在集合中
缺点: hyperloglog主要是进行去重统计,在大数据量下pfadd的结果误差会变大。下图为我本机连接服务器redis存入20000个数据的结果。
void addHll() {
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
int successCount = 0;
int failCount = 0;
for (int i = 0; i < 20000; i++) {
if (redisTemplate.opsForHyperLogLog().add("hll", i+"") == 1) {
successCount++;
} else {
failCount++;
}
}
System.out.printf("成功%d,失败%d",successCount,failCount);
}
输出:
成功14032,失败5968
我们再来看看redis
127.0.0.1:6379> PFCOUNT hll
(integer) 19891
PFCOUNT的值不为20000是因为hyperloglog存在误差 低于0.81%
由此可见hyperloglog并不适合。
布隆过滤器来解决问题
思考一下,我们遇到的问题。该如何解决呢
- 数据量大:我们不能去实际的存储信息。
- 检索时间长:我们不能使用类似于链表等,时间复杂度会随数据量大而增加的数据结构。时间复杂度应为O(1)
解决问题第一版:数组 + hash
我们可以将需要存储的数据经过hashcode
之后取余。将其余数对应的数组位置为1
。
他既没有存储数据,而且数组的检索时间复杂度也为O(1)。
当我们判断数据是否在集合中,只需要判断对应位置是否为1就可以了。
当然,这个图的数组长度只是一个实例,他可以很长很长。
解决问题第二版: 布隆过滤器
我们在第一版中存在着hash冲突的问题。
如果多个数据的hash值相同,或者说hash值取余之后的值相同,那么hash冲突将极其严重。从而当我们判断数据是否在集合中误差较大
如何减少hash冲突
我们可以使用多个hash函数对数据进行hash运算。
当我们判断一个元素是否存在时,会判断多个hash之后的所谓位置值是否为1。都为1的话就视为存在
布隆过滤器的优缺点
优点 :高效地插入和查询,占用空间少
缺点:
- 当数据量逐渐增大的时候,误判率会逐渐上升。如果所有数组都为1,那么所有值都将会被判定存在集合中。
- 不能删除数据。如果删除一个数据的话,可能会将其他多个数据的结构删除。可参考wiki百科
布隆过滤器数组长度以及hash函数数量
根据上文学习我们发现。影响布隆过滤器的条件有 hash函数的个数
以及数组的长度
hash函数的个数
过少:会使误判的概率增加
过多:会是数组快速填满
数组的长度
过短:会快速填满数组,导致误判率大大升高 过长:会占用过多内存,造成不必要浪费
合适的比例
在hackernoon有着介绍
k:hash函数数量 n:数据量 M:数组长度 p:误判率
我们的理想选择是(k =5,m =100M),因为它最大限度地减少了空间和时间/计算要求
计算公式