布隆过滤器(BloomFilter)

布隆过滤器的使用场景

在架构设计中,通常会涉及这样的场景:

  • 词典服务中,查询某个单词是否合法(即是否存在于保存有海量单词的词典文件中)
  • 爬虫服务中,检查某个网页是否已经收录(即是否存在于海量的已爬取网页库中)
  • 文章/商品推荐服务中,检查某篇文章/某个商品是否已经被推荐过(已推荐过的文章或商品不重复推荐);
  • 查询某个元素是否在缓存中存在(假设缓存中的元素非常多)

以上的场景,本质上都是从海量数据中判断某一个元素是否存在。这种场景通常有两种解决方式:

  • 假设空间无限且无需考察元素查找的时间复杂度,那么就将海量元素按连续内存或链表结构进行存储,之后以此查询,此种方式的优势在于简单,劣势也非常明显,那就是无论是空间复杂度还是时间复杂度都非常差,空间/时间复杂度均为O(N),假设数据量非常大时,无论是存储空间的占用还是时间的开销都无法满足业务要求;
  • 采用空间换时间的方式,比如采用HashSet的存储方式,将元素通过某种hash算法获取hashcode,存储到指定的分桶中。这种方式的优势在于时间复杂度有效地得到了提升,即O(1);但缺点依然很明显,其空间存储要求为O(N)。因此,随着元素个数N的增大,分桶数急剧增多,假设数据量很大时,则无法满足(因为如果分桶数不增多,会频繁发生hash碰撞,导致在某些分桶或绝大多数分桶上退化成链表,虽说在JDK8上采用了红黑树的方式提升查找效率,但依然只是一种权衡,因为不过将时间复杂度从O(N)变成了O(logN)。)

尽管上述的方法2可以采用多级HashSet的方式(先采用某种方法,如取模,将数据切分到多台机器的内存中,有效地减少每一个分片的数据量之后,再重复方法2的方式--典型的分库分表的思路)进行水平扩展,但这其实只是一种打补丁的方法,因为数据量会不断扩大,需要提前预估存储水位,之后提前做迁移,存在一定的繁琐性。那么究竟有没有办法可以解决这个问题呢?

答案是肯定的,采用布隆过滤器可以有效地解决以上的问题。

布隆过滤器的原理

其实布隆过滤器的思路是十分直接的,既然一种hash算法可能会出现碰撞,那么采用多个hash算法之后,再次发生碰撞的概率就会急剧减小。详情如下图所示:

布隆过滤器(BloomFilter)_第1张图片

假设我们采用长度为12的数组来做布隆过滤器,注意其长度与存储的元素数无关,因此空间占用为O(1)。

如图所示,我们共采用4种Hash算法,对于Object1而言,其表示为010000010011;同样的,对Object2而言,其表示为001001010010。当我们从布隆过滤器中check Object1是否存在时,仅需要再次进行4次hash运算(这几次Hash计算互相独立,可并行运行),然后检查定位到的数组中的元素是否都为1,如果是,则说明大概率已存在(后边会说为何会出现假真的情况);如果不是,则说明肯定不存在。

刚才提到,可能会出现假真的情况,其产生的根源在于hash碰撞。假设另一个Object3,对于这四种hash算法的结果都与Object1 hash碰撞了,则会出现假真的情况。

布隆过滤器的优点:

  1. 节省存储空间,其对空间的占用是O(1)的,与存储元素数量无关,正因为如此,它是可以存储全部元素的。

布隆过滤器的缺点:

  1. 有误判率,当两个元素按照K种hash算法都碰撞的时候,就出现了误判,即本不存在的数据当成了存在;
  2. 原生的布隆过滤器是不能删除的,但是可以采用改进版在一定程度上解决这个问题,即数组中的元素不是简单的0与1,而是引用计数。

布隆过滤器的使用场景

布隆过滤器的使用场景很多,最为常用的场景就是海量元素集合下,一次写入多次读取的场景下的元素是否存在的判定。

例如:

  • 评论中的敏感词识别:评论分词后,以此通过布隆过滤器检查是否命中敏感词,命中的话进行业务处理(打星号或其他操作);
  • 已推荐文章去重:通过布隆过滤器检查待推荐的文章是否在已推荐文章集合中存在。

布隆过滤器中元素变更或失效的解决方式

原生的布隆过滤器是适用于一次写入/多次读取的场景的,对于元素变更与失效,并没有提供太好的解决办法。但其实元素变更或失效确实在实际的设计中存在着。例如敏感词表中敏感词的解禁,其本质上是将布隆过滤器中的某个元素(敏感词)删除。

常用的解决方式其实刚才已经提到过,使用引用计数,但实际上对于这样的业务场景,可以考虑在空间与业务复杂度之间取一定的折中。比如,将数组中存储的元素变更为Object,Object带有isDeleted属性,如果需要删除的话,将该属性置1。这样的做法相对于引用计数的方式,会增大存储开销,但是对于易读性/可维护性/可扩展性(后续再加过期时间等)提供了有力的抓手。

你可能感兴趣的:(工程架构)