Bloom Filter, Ribbon Filter | 青训营笔记

462 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 9 天。

本文以 CC-BY-SA 4.0 发布。

原本是在想点赞系统的时候查到的,但最后还是没有用上(因为取消点赞困难)。 总之在这里做个笔记。

Filters

Filters(过滤器),顾名思义,就是将不满足某种条件的对象、请求的过滤出去的一种手段。

最简单的过滤器就是一个 lambda 表达式: [1, 2, 3].filter((i) => i != 2)。 而在后端语境下,一个最常用的可以被类比为“过滤器”的便是 Redis 或其它缓存: 在缓存中存在的请求键会被“过滤”掉,只从缓存中读取; 只有缓存中不存在的请求键能够通过过滤器,去数据库读取数据。

Bloom Filters, Ribbon Filters

让我们假想一个情况:我们希望把数据库里不存在的数据的请求都在缓存就过滤掉, 只有数据库里有对应存在的请求才能对数据库进行查询。 很明显,大多情况下,数据库的规模(或是其它实际情形里需要过滤的数据种类等)都太大了, 把数据/主键全都存在缓存里进行过滤的内存开销太大,并不实际。

Bloom filters 和 Ribbon filters 也不能完美处理这种情况,它们进行了妥协: 它们能够保证存在的请求一定可以正常通过,而大多数不存在的请求被过滤掉 ——有小部分不存在的请求会被遗漏,仍然会正常通过。

在集合中不在集合中
正常通过一定小概率(遗漏)
阻止通过不会大概率

单层 Bloom Filter

单层的 Bloom filters 可以看作由一个哈希函数 h1 与一个 map[int]bool 判别表 m1 组成。 如果 m[h(值)] 为真,则 Bloom filter 认为该值可以被通过,反之则不能。 也就是说,单层 Bloom filter 实际上判断的是一个值的哈希需不需要被过滤掉:

  1. 因为哈希的范围大小可以变化(取个模即可),所以判断一个预定范围内的哈希需不需要 被过滤掉只需要一个对应大小的 bitset 即可,空间利用率更高;
  2. 哈希有碰撞的可能性,特别是限制范围(取模)之后,所以 Bloom filter 有误判的可能性:如果哈希发生重合,那么不应被准入的值也会被判通过。

多层 Bloom Filter

无论用的是怎样的哈希函数,哈希碰撞在大多数情况下还是会是有发生的。 这就导致了我们会遗漏很多原本应被阻止通过的请求。

一层过滤器不够,那么我们很直接的想法便是加多几层:

bfilter1 --> bfilter2 --> bfilter3

但是,很明显,这几个过滤器不能使用相同的哈希函数,否则就像 [1, 2, 3].filter((i) => i != 2).filter((i) => i != 2) 一样,后面的过滤器没有任何作用。而使用不同的哈希函数时, 第一个哈希函数发生哈希碰撞的值有可能在第二层被过滤掉, 多层下来便提高了总体的滤过效率。

示例Bloom filter正确值错误值1错误值2
1.h1, m1哈希通过哈希通过哈希通过
2.h2, m2哈希通过哈希不通过哈希通过
3.h3, m3哈希通过哈希通过哈希不通过
结果m1[h1(v)] && m2[h2(v)] && m3[h3(v)]通过不通过不通过

Ribbon filter

Ribbon filter 比 Bloom filter 会节省内存一些。 它和 Bloom filter都有哈希函数 h 和 bitset m, 它们的本质不同之处在于 Ribbon filter 是基于异或操作的:

  1. Bloom filter: 通过第一层并且通过第二层并且通过第三层……
  2. Ribbon filter: 通过第一层异或通过第二层异或通过第三层……

我相信 Ribbon filter 的这个处理方法不符合大多数人的直觉。 实际上,从它的算法处理上我们也可以看出来:

  1. Bloom filter: 我们在把一个加入到集合里时,我们只需要赋值 m[h(值)] = true 就可以了(多层时对每层都进行这样的操作);
  2. Ribbon filter: 我们需要事先知道需要通过过滤器的所有的, 再通过一些神奇的解方程方法把 bitset m 算出来。

也就是说 Ribbon filter 只适用于事先知道的静态数据, 而 Bloom filter 可以在运行时动态添加数据(只需进行 m[h(值)] = true 复制即可)。

当然,两种 filters 都无法(或是难以)移除先前加入的数据: 在加入时数据很明显经过了有损(哈希)处理,而不经额外设计或处理的话从有损数据中移除是不可能的。