位图
通过位图来实现URL去重功能,避免重复爬去相同的网页。
我们要处理的对象是URL,需要支持的操作就两个,添加和查询URL。除了功能方面,我们还要求这两个操作执行效率要高,当处理上亿URL时,内存会非常严重,所以在存储效率上也要高效。
满足条件的数据结构有:
- 散列表
- 红黑树
- 跳表
这些动态数据结构,都支持快速添加和查找数据,但是对内存消耗方面有些大。
比如散列表:当爬去10亿URL,为了去重,将10亿URL存储在散列表中。假设一个URL平均长度为64字节,大约需要60GB内存空间。散列表必须要维持较小装载因子,才能保证不会出现过多散列冲突,导致内存性能下降。而且用链表法解决冲突,还会存储链表。所以将10亿URL构成散列表需要内存可能超过100GB。
优化方向
散列表:中添加、查找数据的时间复杂度已经是O(1)。大O表示法,会忽略掉常数、系数和低阶。
- 链表的节点在内存中是不连续存储的,不能一下加载到CPU缓存中,没办法很好利用CPU告诉缓存,所以性能方面会打折扣。
- 链表的数据都是URL,平均长度为64字节,并且待判重的URL还要进行字符串匹配。
针对内存消耗方面的优化,我们可以使用 布隆过滤器(Bloom Filter)
位图
布隆过滤器本身就是基于位图的,是对位图的一种改进。
示例:我们有1千万个整数,整数范围在1到1亿之间,我们怎么快速查找某个整数是否在1千万个整数中呢?
解决:我们除了用散列表的解决,还可以使用一种“特殊”的散列表,就是 位图。
方式:申请一个1亿、数据类型为布尔类型的数组,将这1千万个整数作为数组下标,将对应的数组值设置成True,比如整数5对应下标为5的数组值设置为true。
布尔类型大小是1字节,实际上我们只需要用一个二进制位(bit)来表示就可以。位图通过下标来定位数据,所以访问效率非常高,并且数字用一个二进制位来表示,所需要的内存空间非常节省。
当使用散列表来存储1千万数据,数据是32位的整形,也就要4个字节存储,总共需要40MB存储空间。使用位图的话只需要1亿个二进制位,也就是12MB作用的存储空间。
但是数字范围在1到100亿之间的1千万个数,那么位图大小就是100亿个二进制位,也就是1200MB消耗内存空间,不降反增。
接下来就需要布隆过滤器。
布隆过滤器
示例:1千万数据,数据范围是1到10亿,布隆过滤器做法是,仍然使用1亿个二进制大小的位图,然后通过哈希函数,对数字进行处理,让它落在1到1亿范围内。设计哈希函数 f ( x ) = x f(x) = x % n f(x)=x x表示数字,n表示位图大小(1亿),也就是数字跟位图的大小进行取模求余。
问题:1亿零1和1得到的结果都是1,这样就无法区分存储了。
解决:我们设计复杂、随机点的哈希函数,还可以用多个哈希函数一块来定位一个数据,来降低冲突概率。
布隆过滤器就是通过K个不同的哈希值,将K个数字作为位图的下标,来表示一个数字的存在。
当我们查询某个数字存在时,同样用K个哈希函数,对这个数字求哈希值,得到K个哈希值,看对应为图中的位置是否都是True,有任意一个不是True,说明该数字不存在。
弊端:会产生误判,只会对存在的情况会有误判,如果某个数字经过布隆过滤器判断不存在就是真的不存在,不会发生误判。不过我们调整哈希函数的个数、位图大小和存储数字的个数之间的比例,这种误判概率就会很低。
URL去重
假设去重网页有10亿,我们可以用100亿二进制位,换成字节是1.2GB,用散列表需要100GB空间。
效率:
- 用多个哈希函数对同一个URL进行处理,只需从内存中读取一次URL,进行多次哈希计算,理论上这个操作是CPU密集型。
- 散列表处理,需要读取散列表冲突链的多个网页URL分布进行字符串匹配,涉及多次的内存数据读取,所以是内存密集型的。
- CPU密集型比内存密集型访问更快。
优化
我们需要支持自动扩容的功能,当数据个数与位图大小的比例超过某个阈值,就重新申请一个新的位图。新来的数字就会被放置在新的位图,但是在判断数据是否存在布隆过滤器中,就需要查看多个位图,执行效率就会降低一些。