目录
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。布隆过滤器有宁可错杀一百,也不能放过一个的性质。讲人话就是属于黑名单的 url 一定能够正确判断它在黑名单中,但不属于黑名单中的 url 也可能会被认为在黑名单中,存在一定的失误率。
至于失误率要保持在多少,数组长度,哈希函数的个数分别要设置多少就需要根据实际情况来选择了
在100亿URL中快速判断某个URL是否存在,可以采用**布隆过滤器(Bloom Filter)**这一高效的概率型数据结构。以下是详细解决方案:
1. 布隆过滤器的核心原理
- 位数组 + 多个哈希函数:使用一个长度为
m
的二进制位数组和k
个独立的哈希函数。 - 插入操作:将URL通过
k
个哈希函数映射到位数组的k
个位置,并将这些位置置为1。 - 查询操作:检查URL对应的
k
个位是否全为1。若全为1,则可能存在(有一定误判率);否则一定不存在。
2. 空间与误判率计算
- 参数选择公式:
-
位数组大小
m
和哈希函数数量k
根据元素数量n
和可接受的误判率p
确定:
-
示例:当
n=100亿
(10^10)、p=1%
时:m ≈ 9.6 × 10^10 bits ≈ 11.4 GB
k ≈ 7
个哈希函数
-
3. 分布式布隆过滤器(应对超大数据)
- 分片策略:将位数组划分为多个分片,存储在不同机器上。
- 一致性哈希:通过哈希函数将URL分配到特定分片,查询时只需访问对应分片,降低单机压力。
4. 优化与改进
- 选择高效哈希函数:如 MurmurHash、Fnv 等,确保计算速度快且分布均匀。
- 压缩位数组:使用Roaring Bitmap等压缩技术减少内存占用。
- 结合持久化存储:布隆过滤器返回“可能存在”时,可进一步查询数据库或磁盘,降低误判影响。
5. 其他备选方案
- Cuckoo Filter:支持删除操作,空间效率更高,但实现复杂度较高。
- 分布式键值存储:如Redis Cluster,但存储100亿URL需要TB级内存,成本较高。
6. 实现步骤
- 初始化参数:根据
n
和p
计算m
和k
。 - 部署分布式位数组:使用Redis或自定义分片服务。
- 插入数据:批量处理URL,通过哈希函数设置位数组。
- 查询流程:哈希URL后检查所有分片对应位是否为1。
下面给你举一个小规模、易于理解的示例,从而把布隆过滤器的插入和查询流程演示一遍。为了便于说明,示例中的参数会刻意设置得很小(在实际生产环境里数字会大得多、哈希函数也更复杂),但原理是一样的。
示例背景设置
- 假设我们有一个位数组(Bit Array)长度为
m = 10
(只有10个bit)。 - 我们准备使用
k = 3
个哈希函数(实际场景中可能更多)。
为了演示,我们先随便给这 3 个哈希函数示例化(在真实场景里,往往用如 MurmurHash、FNV 等专业哈希函数;这里仅用简单的假设函数演示):
- hash1(url):将
url
的 ASCII 码之和,对 10 取模。 - hash2(url):将
url
的长度乘以 7,再对 10 取模。 - hash3(url):将
url
的第一个字符和最后一个字符的 ASCII 码之和,对 10 取模。
注意:这些哈希函数只是为了举例,实际中不会这么简单!
1. 插入流程示例
假设我们要往布隆过滤器里插入一个 URL,比如 url = "google.com"
,来看看会发生什么。
-
初始化位数组
- 初始时,位数组的 10 个 bit 全都是
0
- 用一个示意来表示:
bit array: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ 索引 0 1 2 3 4 5 6 7 8 9
- 初始时,位数组的 10 个 bit 全都是
-
计算哈希值
-
使用前面定义的 3 个哈希函数,分别计算
google.com
的哈希值:-
hash1(“google.com”)
- 假设把字符串
"google.com"
转成 ASCII 码后求和,为了演示,姑且算作1089
(这里不做细算); - 再对 10 取模:
1089 % 10 = 9
- 所以
hash1("google.com") = 9
。
- 假设把字符串
-
hash2(“google.com”)
- 字符串长度为
10
(含.
),再乘以 7 得到70
; 70 % 10 = 0
;- 所以
hash2("google.com") = 0
。
- 字符串长度为
-
hash3(“google.com”)
- 第一个字符是
'g'
,ASCII 码大约是103
; - 最后一个字符是
'm'
,ASCII 码大约是109
; - 两者之和
103 + 109 = 212
; 212 % 10 = 2
;- 所以
hash3("google.com") = 2
。
- 第一个字符是
-
-
-
设置位数组
- 根据这 3 个哈希值,去设置位数组对应位置为
1
:bit_array[9] = 1
bit_array[0] = 1
bit_array[2] = 1
- 此时,位数组变为:
bit array: [1, 0, 1, 0, 0, 0, 0, 0, 0, 1] ↑ ↑ ↑ 索引 0 1 2 3 4 5 6 7 8 9
- 根据这 3 个哈希值,去设置位数组对应位置为
完成插入:接下来我们如果还有别的 URL,也会重复相同的流程,把相应的 bit 置 1。
2. 查询流程示例
接下来,假设我们要查询某个 URL 是否“可能已经在集合里”。
比如说我们先来查询 url = "google.com"
,看看结果:
-
计算哈希
- 跟插入时做同样的 3 次哈希:
hash1("google.com") = 9
hash2("google.com") = 0
hash3("google.com") = 2
- 跟插入时做同样的 3 次哈希:
-
检查位数组
- 查看位数组的
bit_array[9]
、bit_array[0]
、bit_array[2]
是否都是1
。 - 现在这三个位置确实都是
1
。 - 于是我们可以说:“
google.com
在这个布隆过滤器里 可能存在。”
- 查看位数组的
查询结论:由于布隆过滤器会存在误判(可能有别的字符串碰巧把相同的位置都置为了
1
),所以这里只能说“可能存在”。但如果任意一个对应位置是0
,则一定不存在。
再查询一个不存在的示例
假设我们要查询 url = "baidu.com"
(我们事先没有插入过它),来看会发生什么:
-
计算哈希
- 同样用那 3 个示例哈希函数:
hash1("baidu.com")
→ 假设结果是 5hash2("baidu.com")
→ 假设结果是 1hash3("baidu.com")
→ 假设结果是 7
- 同样用那 3 个示例哈希函数:
-
检查位数组
- 查
bit_array[5]
、bit_array[1]
、bit_array[7]
是否都为1
。 - 回头看我们的位数组:
[1, 0, 1, 0, 0, 0, 0, 0, 0, 1]
,可以发现:bit_array[5]
= 0bit_array[1]
= 0bit_array[7]
= 0
- 至少
bit_array[5]
就是 0,所以根本没必要看后面了,说明必定不存在。
- 查
3. 误判是怎么产生的?
在大规模场景中,位数组长度 m
虽然足够大,但是仍然可能存在不同的 URL 分别通过 k
个哈希函数映射后,落到一模一样的几个 bit 上的可能性。结果就是:
- 当你去查询某个并没插入过的“陌生”URL 时,可能它的
k
个哈希值正好撞上了先前插入的若干 URL 设置过的那几个 bit,于是就会误判为“可能存在”。 - 不过,只要有任意一个 bit 是 0,那么就能马上确定它一定没被插入过。
通常,布隆过滤器是通过调整 m
(位数组大小)和 k
(哈希函数数量)来把这种“误判率”尽量压到一个可接受的水平。
- 插入:把要插入的元素(URL)通过
k
个哈希函数映射到位数组的若干位置上,并把那些位置标记为1
。 - 查询:同样通过这
k
个哈希函数查对应位置;若全为1
,则“可能存在”;若有任意0
,则“一定不存在”。 - 误判原理:因为不同元素可能会把同样的位置标记为
1
,导致有些本没插入过的元素,也查到的都是1
。
总结
布隆过滤器以约11.4GB内存实现100亿URL的快速检索(1%误判率),是空间与时间效率最优解。若需精确判断,可结合数据库二次验证。此方案适用于爬虫去重、缓存穿透防护等场景。