布隆过滤器原理及实践
Posted Java程序员老张
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了布隆过滤器原理及实践相关的知识,希望对你有一定的参考价值。
1 背景
现在有海量的数据,而这些数据的大小已经远远超出了服务器的内存,现在再来一条数据,如何快速高效判断这条数据在不在其中?
如果这些数据是存在数据库中的,考虑索引,分库分表;或者考虑其他目前主流的云数据库平台;或者基于内存存储的redis,即使redis读取速度快,但也会存在大key问题(hash,list,set等存储中value值过多,读写bigkey会导致超时严重,甚至阻塞服务)
如果服务器的内存足够大,那么用HashMap是一个不错的解决方案,理论上的时间复杂度可以达到O(1),但是现在数据的大小已经远远超出了服务器的内存,而且这是建立在单体基础上的,随着分布式架构,一个map已经没法满足这个需求,实现共享又是不可行的。布隆过滤器应运而生,可应对海量数据判断是否存在。
2 应用场景
1 判断一个元素在海量级数据中是否存在
2 Google Chrome 使用布隆过滤器识别恶意 URL,加速安全浏览服务
3 Medium.com(专注于英文写作和阅读的平台) 使用布隆过滤器避免推荐给用户已经读过的文章,或者判断用户是否阅读过某视频或文章,比如抖音或头条
4 邮箱场景 反垃圾邮件,从数十亿个邮件列表中判断某邮箱是否垃圾邮箱
5 在网络爬虫里,对URL去重,避免爬取相同的URL地址
6 数据库防止穿库:Google BigTable,Apache HBbase 和 Apache Cassandra 使用布隆过滤器减少对不存在的行和列的磁盘查找,提高数据库查询操作的性能
7 缓解缓存穿透问题
当我们把一些热点数据放在 Redis 中缓存, 通常一个请求我们会先查询缓存,而不用直接读取数据库,这是提升性能最简单也是最普遍的做法,但是如果一直请求一个不存在的缓存,那就会有大量请求直接打到数据库上,造成缓存穿透,布隆过滤器也可以用来解决此类问题,当对key查询的请求量很大时,很可能对DB造成影响。
需要注意的是以上问题通过布隆过滤器不能完全解决,我们只能将其控制在一个可以容忍的范围内。
3 基本原理
术语概念
1 位图:通过数组下标来定位数据,使用比特位来标记(映射)这些数据。布隆过滤器就是一种基于位图逻辑的一个很长的二进制向量和一系列随机映射函数
2 期望插入数目:表示预计放入的元素数量,需要对场景所需数据量进行预估。当实际数量超过这个值时,误判率就会提升,所以需要提前设置一个较大的数值避免超出导致误判率升高
3 误差率:允许达到的误差率是千分之一还是万分之一,误差率越低,所需数组长度就会越长,所需要的空间就越大
4 hash函数的特性:
4.1 如果两个hash值是不相同的(根据同一函数),那么这两个散列值的原始输入也是不相同的。这个特性是hash函数具有确定性的结果。
4.2 散列函数的输入和输出不是唯一对应关系的,如果两个hash值相同,两个输入值很可能是相同的,但也可能不同,这种情况称为“Hash碰撞(collision)”。
底层原理
BloomFilter 是由一个固定大小的二进制向量或者位图(bitmap)和一系列映射函数组成的。它可以用来判断某个元素是否在集合内,布隆过滤器的基础数据结构是一个比特向量。每个占位符用二进制来表示,内存空间非常节省。
但是hash函数就存在冲突问题,可以使用更复杂的hash函数,同时用多个hash函数一起定位一条数据,也就是通过n个二进制位来表示一个数字是否存在,这样也可以降低冲突的概率。
考虑这样一种情况,随着插入元素越来越多,极端情况下布隆过滤器空间占满,会造成每一次查询都会返回1,这样就会造成严重的误差。因此,这就意味着初始化这个数组的长度严重取决于期望预计添加元素的数量,即数组长度要大于期望插入元素的长度。
推荐一款计算bit数组长度的小工具:https://hur.st/bloomfilter/?n=10000&p=1.0E-3&m=&k=3
关系趋势
1 误差率(p)和期望元素个数(n),随着期望插入个数的提升,误差率也会随之提升
2 误差率(p)和hash函数次数(k),随着hash函数次数的提升,误差率会随之降低
总结:预期总量一定的情况下,hash次数越多,误差率越低。但是hash次数过多又会降低查询速度
特点
优点
1 由于存放的不是完整的数据,而是0、1这样的二进制,所以占用的内存很少;
2 只需要做hash函数算法,所以新增,查询速度快。
缺点
1 随着数据的增加,误判率随之增加;
2 无法删除数据;当实际元素数量超过初始化数量时,应该对布隆过滤器进行重建,重新分配一个 size 更大的过滤器,再将所有的历史元素批量 add 进行
3 只能判断数据是否一定不存在,而无法判断数据是否一定存在。
4 原理演示
算法
1. 首先需要k个hash函数,每个函数可以把key散列成为1个整数;
2. 初始化时,需要一个长度为n比特的数组,每个比特位初始化为0;
3. 某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位的值修改为1;
4. 判断某个key是否在集合时,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中。
图形演示
1 现在我们新建一个长度为16的布隆过滤器,默认值都是0,就像下面这样:
2 现在需要添加一个数据:
我们通过某种hash计算方式,比如Hash1,计算出了Hash1(数据)=5,我们就把下标为5的格子改成1,就像下面这样:
我们又通过某种hash计算方式,比如Hash2,计算出了Hash2(数据)=9,我们就把下标为9的格子改成1,就像下面这样:
还是通过某种计算方式,比如Hash3,计算出了Hash3(数据)=2,我们就把下标为2的格子改成1,就像下面这样:
这样,刚才添加的数据就占据了布隆过滤器“5”,“9”,“2”三个格子。
可以看出,仅仅从布隆过滤器本身而言,根本没有存放完整的数据,只是运用一系列随机映射函数计算出位置,然后填充二进制向量。
3 判断是否存在
只需利用上面的三种固定的计算方式,计算出这个数据占据哪些格子,然后看看这些格子里面放置的是否都是1,如果有一个格子不为1,那么就代表这个数字不在其中。
4 判断是否一定存在
但是有一个问题需要注意,即使这些格子里面放置的都是1,不一定代表给定的数据一定存在,也许其他数据经过这三种固定的计算方式算出来的结果也是相同的。
5 删除:比如要删除数据,把“5”,“9”,“2”三个格子都改成了0,但是可能其他的数据也映射到了“5”,“9”,“2”三个格子,这样就会造成数据错误,所以没法删除。
5 实现方式
1 引入guava包实现
2 利用redis来实现:Redis 因其支持 setbit 和 getbit 操作,且基于纯内存操作以至于性能高等特点,因此天然就可以作为布隆过滤器来使用
3 使用redis布隆过滤器插件来实现。redis4.0之后支持插件支持布隆过滤器 https://github.com/RedisBloom/RedisBloom
6 代码演示 show me the code
1 根据预期插入数目和误差率计算big数组长度
2 取模计算所占用位置
3 向布隆过滤器中添加值
4 在布隆过滤器中判断某值是否存在
7 温馨提示
1 针对大value做拆分:
布隆过滤器的不当使用极易产生大 Value,增加 Redis 阻塞风险,因此生成环境中建议对体积庞大的布隆过滤器进行拆分。
拆分的形式方法多种多样,但是本质是不要将 Hash(Key) 之后的请求分散在多个节点的多个小 bitmap 上,而是应该拆分成多个小 bitmap 之后,对一个 Key 的所有哈希函数都落在这一个小 bitmap 上。例如一千万数据进行拆分redis,就需要1000个桶,每个桶10000个bit,这样一个桶17k,一千个桶只需要17M左右。
所以通用的方案:1kw的数据量(分1k片,每个分片1w的数据量),做3次hash函数,设置误差率0.001,这样占用的内存大小17k*1000=17M。
8 总结
1 布隆过滤器可以帮助我们解决一些海量数据场景下的判断是否存在问题
2 他的特点是不一定存在,但是一定不存在,所以在使用过程中会存在误差率
3 他的查询速度快,占用内存空间小,但是通过一个大桶这样的不合理使用也会造成大key问题,导致阻塞,所以要进行分片
4 实现方式可以由guava、redis、redis自带bloom-filter插件实现
布隆过滤器原理及实现
布隆过滤器原理及实现
前言
最近有朋友面试经常被问到redis缓存穿透怎么解决,什么是redis缓存穿透呢?就是客户端去访问一个缓存和数据库都不存在的 key这样的查询直接打到数据库上。解决办法很多。
1接口参数校验,
2在缓存中设置空值,
3布隆过滤器。
本章咱们就来看下布隆过滤器怎么解决的
什么是布隆过滤器
布隆过滤器可以快速的从海量中数据校验一个数据是否存在。
它内部结构由多个hash函数和一组bit数组成
每个bit位置默认是0
下面请看图
实现过程
1、把所有数据的位置信息添加到bit数组
每条数据经过hash函数运算,有几个函数就计算几次,
每次计算都会算出对应的位置信息,然后把该位置由0置为1,
2、校验单个数据是否存在布隆过滤器里
跟添加位置信息一样经过多个函数算出对应的值信息
如果每个位置信息都是1则布隆过滤器判断该数据存在
如果都为0或者有一个位置信息为0则判断数据不存在
优势:
1、仅仅保留保留数据的位置信息,空间效率极高
2、信息安全性较高,不能根据位置信息反算出来数据
3、查询效率极高,时间复杂度0(n)
缺点:
存在一定的误判:当一个不存在的数据经过hash运算算出的位置信息 都是1的时候,这时候就发生了hash冲突,布隆过滤器就会误判,hash冲突我们无法避免,所以误判无法避免,hash函数越多误判率越低,这个误判率我们可以手动配置,但是记住一点误判率越低bit数组越大,hash函数越多相应的性能就会降低。
数据删除困难:当我们要删除一个数据的时候不能仅把他对应的位置置为0,因为该位置可能还是别的数据的位置信息。所以不能物理删除。
看到这里,可能会有人问既然布隆过滤器没办法避免误判率,那么我们为什么要用它或者什么时候要用它呢?
它只会误判不存在的数据,不会误判存在的数据
首先回到我们前言说的redis缓存穿透的问题,当有人恶意攻击你的服务器缓存,每秒上万的用不存在的k来访问我们缓存时候,我们在缓存前面加上布隆过滤器,当有真实存在的数据一定不会误判,这样经过布隆过滤器后可能只有很少的非法数据访问到我们的服务器,服务器也可以承受。
经典应用场景
1、字处理软件,需要检查一个英语单词是否正确
2、网站非法url过滤
3、订单物流单号查询
4、FBI,查询一个嫌疑人是否存在嫌疑名单上
最后来下看demo
所需依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
代码实现
package com.teamer.servicetm.controller;
import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class BloomFilterTest {
private static final int num=1000000;
public static void main(String[] args) {
/**
创建一个存储string的布隆过滤器,初始化大小为num,默认误判率为0.03,如果我们想把误判率可以这样写
BloomFilter<String> bloomFilter=BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),num,0.01D);
*/
BloomFilter<String> bloomFilter=BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),num);
//创建两个list容器 用来验证过滤器、以及他的误判率
List<String> list=new ArrayList<>(num);
List<String> list1=new ArrayList<>(num);
for(int i=0;i<num;i++){
String uuid= UUID.randomUUID().toString();
bloomFilter.put(uuid);
list.add(uuid);
list1.add(uuid);
}
int nums=0;
int trueNum=0;//正确的个数
int wrongNum=0;//错误的个数
for(int i=0;i<10000;i++){
String str=i%100==0?list.get(i):UUID.randomUUID().toString();
//布隆过滤器判断如果为true证明布隆过滤器认为存在
if(bloomFilter.mightContain(str)){
nums++;
//再用list1判断下如果list1也认为存在则证明布隆过滤器判断正确,反之判断错误
if(list1.contains(str)){
trueNum++;
}else{
wrongNum++;
}
}
}
System.out.println("布隆过滤器校验的个数为"+nums);
System.out.println("正确的个数为"+trueNum);
System.out.println("误判的个数为"+wrongNum);
}
}
这里只是讲下大致原理过程,真正实现时候我们要多个线程去一块初始化数据,需要用到线程池化技术、锁(因为布隆过滤器是线程不安全的)、线程协作等。有兴趣的可以了解下Fork/Join。
以上是关于布隆过滤器原理及实践的主要内容,如果未能解决你的问题,请参考以下文章