这是我参与「第五届青训营 」伴学笔记创作活动的第 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 实际上判断的是一个值的哈希需不需要被过滤掉:
- 因为哈希的范围大小可以变化(取个模即可),所以判断一个预定范围内的哈希需不需要 被过滤掉只需要一个对应大小的 bitset 即可,空间利用率更高;
- 哈希有碰撞的可能性,特别是限制范围(取模)之后,所以 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 是基于异或操作的:
- Bloom filter: 通过第一层并且通过第二层并且通过第三层……
- Ribbon filter: 通过第一层异或通过第二层异或通过第三层……
我相信 Ribbon filter 的这个处理方法不符合大多数人的直觉。 实际上,从它的算法处理上我们也可以看出来:
- Bloom filter: 我们在把一个
值加入到集合里时,我们只需要赋值m[h(值)] = true就可以了(多层时对每层都进行这样的操作); - Ribbon filter: 我们需要事先知道需要通过过滤器的所有的
值, 再通过一些神奇的解方程方法把 bitsetm算出来。
也就是说 Ribbon filter 只适用于事先知道的静态数据,
而 Bloom filter 可以在运行时动态添加数据(只需进行 m[h(值)] = true 复制即可)。
当然,两种 filters 都无法(或是难以)移除先前加入的数据: 在加入时数据很明显经过了有损(哈希)处理,而不经额外设计或处理的话从有损数据中移除是不可能的。