|
2 | 2 |
|
3 | 3 | ## 场景
|
4 | 4 |
|
5 |
| -假设你现在要处理这样一个问题,你有一个网站并且拥有`很多`访客,每当有用户访问时,你想知道这个ip是不是第一次访问你的网站。 |
| 5 | +假设你现在要处理这样一个问题,你有一个网站并且拥有`很多`访客,每当有用户访问时,你想知道这个 ip 是不是第一次访问你的网站。 |
6 | 6 |
|
7 | 7 | ### hashtable 可以么
|
8 | 8 |
|
9 |
| -一个显而易见的答案是将所有的 IP 用 hashtable 存起来,每次访问都去 hashtable 中取,然后判断即可。但是题目说了网站有`很多`访客, |
10 |
| -假如有10亿个用户访问过,假设 IP 是 IPV4, 那么每个 IP 的长度是 4 byte,那么你一共需要4 * 1000000000 = 4000000000Bytes = 4G 。 |
| 9 | +一个显而易见的答案是将所有的 IP 用 hashtable 存起来,每次访问都去 hashtable 中取,然后判断即可。但是题目说了网站有`很多`访客,假如有 10 亿个用户访问过,假设 IP 是 IPV4, 那么每个 IP 的长度是 4 byte,那么你一共需要 4 \* 1000000000 = 4000000000Bytes = 4G 。 |
11 | 10 |
|
12 | 11 | 如果是判断 URL 黑名单,由于每个 URL 会更长(可能远大于上面 IPV4 地址的 4 byte),那么需要的空间可能会远远大于你的期望。
|
13 | 12 |
|
14 | 13 | ### bit
|
15 | 14 |
|
16 |
| -另一个稍微难想到的解法是bit, 我们知道bit有 0 和 1 两种状态,那么用来表示**存在**与**不存在**再合适不过了。 |
| 15 | +另一个稍微难想到的解法是 bit, 我们知道 bit 有 0 和 1 两种状态,那么用来表示**存在**与**不存在**再合适不过了。 |
17 | 16 |
|
18 |
| -假如有 10 亿个 IP,就可以用 10 亿个 bit 来存储,那么你一共需要 1 * 1000000000 = (4000000000 / 8) Bytes = 128M, 变为原来的1/32, 如果是存储URL这种更长的字符串,效率会更高。 问题是,我们怎么把 IPV4 和 bit 的位置关联上呢? |
| 17 | +假如有 10 亿个 IP,就可以用 10 亿个 bit 来存储,那么你一共需要 1 \* 1000000000 = (4000000000 / 8) Bytes = 128M, 变为原来的 1/32, 如果是存储 URL 这种更长的字符串,效率会更高。 问题是,我们怎么把 IPV4 和 bit 的位置关联上呢? |
19 | 18 |
|
20 | 19 | 比如`192.168.1.1` 应该是用第几位表示,`10.18.1.1` 应该是用第几位表示呢? 答案是使用哈希函数。
|
21 | 20 |
|
|
27 | 26 |
|
28 | 27 | > 我们可以通过优化散列函数来解决
|
29 | 28 |
|
30 |
| -2. 当元素不是整型(比如URL)的时候,BitSet就不适用了 |
| 29 | +2. 当元素不是整型(比如 URL)的时候,BitSet 就不适用了 |
31 | 30 |
|
32 |
| -> 我们还是可以使用散列函数来解决, 甚至可以多hash几次 |
| 31 | +> 我们还是可以使用散列函数来解决, 甚至可以多 hash 几次 |
33 | 32 |
|
34 | 33 | ### 布隆过滤器
|
35 | 34 |
|
36 |
| -布隆过滤器其实就是`bit + 多个散列函数`。k 次 hash(ip) 会生成多个索引,并将其 k 个索引位置的二进制置为 1。 如果经过 k 个索引位置的值都为 1,那么认为其**可能存在**(因为有冲突的可能)。 如果有一个不为1,那么**一定不存在**(一个值经过散列函数得到的值一定是唯一的),这也是布隆过滤器的一个重要特点。也就是说布隆过滤器回答了:**可能存在** 和 **一定不存在** 的问题。 |
| 35 | +布隆过滤器其实就是`bit + 多个散列函数`。k 次 hash(ip) 会生成多个索引,并将其 k 个索引位置的二进制置为 1。 |
| 36 | + |
| 37 | +- 如果经过 k 个索引位置的值都为 1,那么认为其**可能存在**(因为有冲突的可能)。 |
| 38 | +- 如果有一个不为 1,那么**一定不存在**(一个值经过散列函数得到的值一定是唯一的),这也是布隆过滤器的一个重要特点。 |
| 39 | + |
| 40 | +也就是说布隆过滤器回答了:**可能存在** 和 **一定不存在** 的问题。 |
37 | 41 |
|
38 | 42 | 
|
39 | 43 |
|
40 | 44 | 从上图可以看出, 布隆过滤器本质上是由**一个很长的二进制向量**和**多个哈希函数**组成。
|
41 | 45 |
|
42 |
| -由于没有 hashtable 的100% 可靠性,因此这本质上是一种**可靠性换取空间的做法**。除了可靠性,布隆过滤器删除起来也比较麻烦。 |
| 46 | +由于没有 hashtable 的 100% 可靠性,因此这本质上是一种**可靠性换取空间的做法**。除了可靠性,布隆过滤器删除起来也比较麻烦。 |
| 47 | + |
| 48 | +### 误报 |
| 49 | + |
| 50 | +上面提到了布隆过滤器回答了:**可能存在** 和 **一定不存在** 的问题。 因此当回答是**可能存在**的时候你该怎么做?一般而言, 为了宁可错杀一千,也不放过一个,我们认为他存在。 这个时候就产生了**误报**。 |
| 51 | + |
| 52 | +误报率和二进制向量的长度成反比。 |
43 | 53 |
|
44 | 54 | ### 布隆过滤器的应用
|
45 | 55 |
|
46 | 56 | 1. 网络爬虫
|
47 | 57 |
|
48 |
| -判断某个URL是否已经被爬取过 |
| 58 | +判断某个 URL 是否已经被爬取过 |
49 | 59 |
|
50 |
| -2. K-V数据库 判断某个key是否存在 |
| 60 | +2. K-V 数据库 判断某个 key 是否存在 |
51 | 61 |
|
52 | 62 | 比如 Hbase 的每个 Region 中都包含一个 BloomFilter,用于在查询时快速判断某个 key 在该 region 中是否存在。
|
53 | 63 |
|
|
57 | 67 |
|
58 | 68 | > 从这个算法大家可以对 tradeoff(取舍) 有更入的理解。
|
59 | 69 |
|
| 70 | +4. 恶意网站识别 |
| 71 | + |
| 72 | +总之, 如果你需要判断**一个项目是否在一个集合中出现过,并且需要 100% 确定没有出现过,或者可能出现过**,就可以考虑使用布隆过滤器。 |
| 73 | + |
| 74 | +### 代码 |
| 75 | + |
| 76 | +```java |
| 77 | +public class MyBloomFilter { |
| 78 | + private static final int DEFAULT_SIZE = 2 << 31 ; |
| 79 | + private static final int[] seeds = new int [] {3,5,7,11,13,19,23,37 }; |
| 80 | + private BitSet bits = new BitSet(DEFAULT_SIZE); |
| 81 | + private SimpleHash[] func = new SimpleHash[seeds.length]; |
| 82 | + |
| 83 | + public static void main(String[] args) { |
| 84 | + //使用 |
| 85 | + String value = "www.xxxxx.com" ; |
| 86 | + MyBloomFilter filter = new MyBloomFilter(); |
| 87 | + System.out.println(filter.contains(value)); |
| 88 | + filter.add(value); |
| 89 | + System.out.println(filter.contains(value)); |
| 90 | + } |
| 91 | + //构造函数 |
| 92 | + public MyBloomFilter() { |
| 93 | + for ( int i = 0 ; i < seeds.length; i ++ ) { |
| 94 | + func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]); |
| 95 | + } |
| 96 | + } |
| 97 | + //添加网站 |
| 98 | + public void add(String value) { |
| 99 | + for (SimpleHash f : func) { |
| 100 | + bits.set(f.hash(value), true ); |
| 101 | + } |
| 102 | + } |
| 103 | + //判断可疑网站是否存在 |
| 104 | + public boolean contains(String value) { |
| 105 | + if (value == null ) { |
| 106 | + return false ; |
| 107 | + } |
| 108 | + boolean ret = true ; |
| 109 | + for (SimpleHash f : func) { |
| 110 | + //核心就是通过“与”的操作 |
| 111 | + ret = ret && bits.get(f.hash(value)); |
| 112 | + } |
| 113 | + return ret; |
| 114 | + } |
| 115 | +} |
| 116 | +``` |
| 117 | + |
| 118 | +## 总结 |
| 119 | + |
| 120 | +布隆过滤器回答了:**可能存在** 和 **一定不存在** 的问题。本质是一种空间和准确率的一个取舍。实际使用可能会有误报的情况, 如果你的业务可以接受误报,那么使用布隆过滤器进行优化是一个不错的选择。 |
0 commit comments