Bloom Filter

1.引子

          假设要你写一个网络蜘蛛(web crawler)。由于网络间的链接错综复杂,蜘蛛在网络间爬行很可能会形成“环”。为了避免形成“环”,就需要知道蜘蛛已经访问过那些URL。给一个URL,怎样知道蜘蛛是否已经访问过呢?稍微想想,就会有如下几种方案:

          1. 将访问过的URL保存到数据库。
          2. 用HashSet将访问过的URL保存起来。那只需接近O(1)的代价就可以查到一个URL是否被访问过了。
          3. URL经过MD5或SHA-1等单向哈希后再保存到HashSet或数据库。
          4. Bit-Map方法。建立一个BitSet,将每个URL经过一个哈希函数映射到某一位。


         方法1~3都是将访问过的URL完整保存,方法4则只标记URL的一个映射位。
         方法1的缺点:数据量变得非常庞大后关系型数据库查询的效率会变得很低。而且每来一个URL就启动一次数据库查询是不是太小题大做了?
         方法2的缺点:太消耗内存。随着URL的增多,占用的内存会越来越多。就算只有1亿个URL,每个URL只算50个字符,就需要5GB内存。
         方法3:由于字符串经过MD5处理后的信息摘要长度只有128Bit,SHA-1处理后也只有160Bit,因此方法3比方法2节省了好几倍的内存。
         方法4消耗内存是相对较少的,但缺点是单一哈希函数发生冲突的概率太高。还记得数据结构课上学过的Hash表冲突的各种解决方法么?若要降低冲突发生的概率到1%,就要将BitSet的长度设置为URL个数的100倍。
          废话说到这里,下面引入本篇的主角——Bloom Filter。其实上面方法4的思想已经很接近Bloom Filter了。方法四的致命缺点是冲突概率高,为了降低冲突的概念,Bloom Filter使用了多个哈希函数,而不是一个。

2.原理

          查找或判断一个元素是否存在于一个指定集合中,这是计算机科学中一个基本常见问题。通常,我们会采用线性表(数组或链表)、树(二叉树、堆、红黑树、 B+/B-/B*树)等数据结构对所有元素进行存储,并在其上进行排序和查找。这里的查找时间复杂性通常都是O(n)或O(logn)的,如果集合元素非常庞大,不仅查找速度非常慢,对内存空间的需求也非常大。假设有10亿个元素,每个元素节点占用N个字节,则存储这个集合大致需求N GB内存。大家可能很快会想到hashtable,它的查找时间复杂性是O(1)的,可以对元素进行映射索引并定位,但它并没有减少内存需求量。hash 函数的一个问题是可能会发生碰撞,即两个不同的元素产生相同的hash值,在某些场合下需要通过精确比较来解决这个问题。
          实际上,判断一个元素是否存在于一个指定集合中,可能并不需要把所有集合元素的原始信息都保存下来,我们只需要记住“存在状态”即可,这往往仅仅需要几个bit就可表示。Hash函数可将一个元素映射成一个位数组中一个点,为了降低碰撞率可采用多个hash函数将元素映射成多个点。这样一来,只要看看几个位点是0或1 就可以判断某个元素是否存在于集合当中。这就是Bloom filter的基本思想,不仅可大大缩减内存空间,查找速度非常快。Bloom filter使用一个位数组来记录元素存在状态,并使用一组hash函数(h1, h2, hk...)来对元素进行位映射。插入元素时,对该元素分别进行K次hash计算,并将映射到位数组的相应bit置1。查找元素时,任何其中一个映射位为 0则表示该元素不存在于集合当中,只要当所有映射位均为1时才表示该元素有可能存在于集合当中。换句话说,如果Bloom filter判断一个元素不在集合中,那肯定就不存在;而如果判断存在,则不一定存在,虽然这个概率很低。这个问题是由hash函数会发生碰撞的特性所决定的,它造成了Bloom filter的错误率产生。这个错误率可通过改变Bloom filter数组大小,或改变hash函数个数进行调节控制。由此可见,Bloom filter也不是完美的,它的高效也是有一定代价的,它通过容忍一定的错误率发生换取了存储空间的极大节省。另外,Bloom filter不能支持元素的删除操作,如果删除会影响其他元素的存在性正确判断。因此,Bloom Filter不适合那些“零错误”的应用场合,但是这个错误是正向的(false positive),不会发生反向的错误(false negative),判断元素不存在集合中是绝对正确的。Bloom filter使用可控的错误率获得了空间的极大节省和极快的查找性能,得到广泛应用也是理所当然的。

3.参数的确定

          还有一个比较重要的问题,如何根据输入元素个数n,确定位数组m的大小及hash函数个数。
          当hash函数个数k=(ln2)*(m/n)时错误率最小。在错误率不大于E的情况 下,m至少要等于n*lg(1/E)才能表示任意n个元素的集合。但m还应该更大些,因为还要保证bit数组里至少一半为0,则m应 该>=nlg(1/E)*lge 大概就是nlg(1/E)1.44倍(lg表示以2为底的对数)。
          举个例子我们假设错误率为0.01,则此时m应大概是n的13倍。这样k大概是8个。
          注意这里m与n的单位不同,m是bit为单位,而n则是以元素个数为单位(准确的说是不同元素的个数)。通常单个元素的长度都是有很多bit的。所以使用bloom filter内存上通常都是节省的。

4.优缺点

          与其它数据结构相比较,Bloom filter的最大优点是空间效率和查找时间复杂性,它的存储空间和插入/查询时间都是常数。Hash函数之间没有相关性,可以方便地由硬件并行实现。Bloom filter不需要存储元素本身,在某些对保密要求非常严格的场合有优势。另外,Bloom filter一般都可以表示大数据集的全集,而其它任何数据结构都难以做到。
           Bloom filter的缺点和优点一样显著,首先就是错误率。随着插入的元素数量增加,错误率也随之增加。虽然可以通过增加位数组大小或hash函数个数来降低错误率,但同时也时影响空间效率和查找性能,而且这个错误率是无法从根本上消除的。这使得要求“零错误”的场合无法应用Bloom filter。其次,一般情况下不能从Bloom filter中删除元素。一方面是我们不能保证删除的元素一定存在Bloom filter中,另一方面是不能保证安全地删除元素,可能会对其他元素产生影响,究其原因还是hash函数可能产生的碰撞造成的。Counting Bloom filter可以在一定程度上支持元素删除,但保证安全地删除元素并非如此简单,它也不能从根本上解决这个问题,而且计数器回绕也会有问题。这两方面也是目前Bloom filter的重点研究方向,有不少工作,使得出现了很多Bloom filter的变种。

5.应用

             适用范围:可以用来实现数据字典,进行数据的判重,或者集合求交集          

             Bloom filter被广泛应用于各种领域,比如拼写检查、字符串匹配算法、网络包分析工具、Web Cache、文件系统、存储系统等,这里着重介绍一下Bloom filter在重复数据删除中的应用。主流的重复数据删除技术的基本原理是对文件进行定长或变长分块,然后利用hash函数计算数据块指纹,如果两个数据块指纹相同则认为是重复数据块(同样这里存在数据碰撞问题),只保存一个数据块副本即可,其他相同数据块使用该副本索引号表示,从而实现数据缩减存储空间并提高存储效率。为了查询一个数据块是否重复或者已经存在,需要计算数据块指纹并进行查找,并记录所有唯一数据块的指纹。举一个例子:32TB的数据,平均数据块大小为8KB,每个数据块使用MD5和SHA1计算两个指纹并用64位整数表示唯一块号则共占用44字节((128+160+64)/8),则总共最多需要176GB(32TB/8KB * 44 Byte)的存储空间来保存数据块信息。现在的去重系统数据容量通常多达数十到数百TB,如果把数据块信息全部保存在内存中,显然对内存的需求量非常巨大,出于成本考虑这对商业产品是不现实的。因此,为了在成本和性能两方面作折中,通常的做法是把数据块信息保存在磁盘或SSD上,使用一定内存量作 Cache缓存数据块指纹,利用时间局部性和空间局部性来提高查找性能。这种方法的一个关键问题是,如果新的数据块是不重复的,查找时会出现Cache不命中,从而引起大量的磁盘读写操作。由于磁盘或SSD性能要远远小于内存的,对查找性能影响非常大。Bloom filter可以有效解决这个问题,DataDomain中的Summary Vector就是采用Bloom filter来实现的。对于前面的例子,一个数据块用3个hash函数计算指纹最多占用3个位,则Bloom filter仅需要1.5GB = 32TB/8KB * 3 /8 bytes的内存空间,这即使对于普通的PC机都不是问题。引入Bloom filter机制后,对于一个新数据块,首先查找Bloom filter,如果未命中则说明这是一个新的唯一数据块,直接保存数据块和并Cachr数据块信息即可;如果命中,则说明这有可能是一个重复数据块,需要通过进一步的hash或tree查找进行确认,此时需要Cache与Disk进行交互。受益于Bloom filter以及Cache,DataDomain系统可以减少99%的磁盘访问,从而利用少量的内存空间大幅提高了数据块查重性能。


以下代码来自:http://en.literateprograms.org/Bloom_filter_%28C%29?oldid=16893

/*----------------------------------------bloom.h------------------------------*/
#ifndef __BLOOM_H__
#define __BLOOM_H__

#include<stdlib.h>

typedef unsigned int (*hashfunc_t)(const char *);
typedef struct {
	size_t asize;
	unsigned char *a;
	size_t nfuncs;
	hashfunc_t *funcs;
} BLOOM;

BLOOM *bloom_create(size_t size, size_t nfuncs, ...);
int bloom_destroy(BLOOM *bloom);
int bloom_add(BLOOM *bloom, const char *s);
int bloom_check(BLOOM *bloom, const char *s);

#endif


/*----------------------------------------bloom.c------------------------------*/
#include<limits.h>
#include<stdarg.h>

#include"bloom.h"

//CHAR_BIT is the number of bits for smallest object that is not a bit-field (byte)
#define SETBIT(a, n) (a[n/CHAR_BIT] |= (1<<(n%CHAR_BIT)))
#define GETBIT(a, n) (a[n/CHAR_BIT] & (1<<(n%CHAR_BIT)))

BLOOM *bloom_create(size_t size, size_t nfuncs, ...)
{
	BLOOM *bloom;
	va_list l;
	int i;
	
	if(!(bloom=malloc(sizeof(BLOOM)))) 
		return NULL;
	if(!(bloom->a=calloc((size+CHAR_BIT-1)/CHAR_BIT, sizeof(char)))) {
		free(bloom);
		return NULL;
	}
	if(!(bloom->funcs=(hashfunc_t*)malloc(nfuncs*sizeof(hashfunc_t)))) {
		free(bloom->a);
		free(bloom);
		return NULL;
	}

	va_start(l, nfuncs);
	for(i=0; i<nfuncs; ++i) {
		bloom->funcs[i]=va_arg(l, hashfunc_t);
	}
	va_end(l);

	bloom->nfuncs=nfuncs;
	bloom->asize=size;

	return bloom;
}

int bloom_destroy(BLOOM *bloom)
{
	free(bloom->a);
	free(bloom->funcs);
	free(bloom);

	return 0;
}

int bloom_add(BLOOM *bloom, const char *s)
{
	size_t i;

	for(i=0; i<bloom->nfuncs; ++i) {
		SETBIT(bloom->a, bloom->funcs[i](s)%bloom->asize);
	}

	return 0;
}

int bloom_check(BLOOM *bloom, const char *s)
{
	size_t i;

	for(i=0; i<bloom->nfuncs; ++i) {
		if(!(GETBIT(bloom->a, bloom->funcs[i](s)%bloom->asize))) 
			return 0;
	}

	return 1;
}

/*----------------------------------------test.c------------------------------*/
#include <stdio.h>
#include <string.h>

#include "bloom.h"

unsigned int sax_hash(const char *key)
{
	unsigned int h=0;

	while(*key)
		h ^= (h<<5)+(h>>2) + (unsigned char)*key++;

	return h;
}

unsigned int sdbm_hash(const char *key)
{
	unsigned int h=0;

	while(*key) 
		h = (unsigned char)*key++ + (h<<6) + (h<<16) - h;

	return h;
}

int main(int argc, char *argv[])
{
	FILE *fp;
	char line[1024];
	char *p;
	BLOOM *bloom;
	
	if(argc<2) {
		fprintf(stderr, "ERROR: No word file specified\n");
		return EXIT_FAILURE;
	}

	if(!(bloom=bloom_create(2500000, 2, sax_hash, sdbm_hash))) {
		fprintf(stderr, "ERROR: Could not create bloom filter\n");
		return EXIT_FAILURE;
	}

	if(!(fp=fopen(argv[1], "r"))) {
		fprintf(stderr, "ERROR: Could not open file %s\n", argv[1]);
		return EXIT_FAILURE;
	}

	while(fgets(line, 1024, fp)) {
		if((p=strchr(line, '\r'))) 
			*p='\0';
		if((p=strchr(line, '\n'))) 
			*p='\0';

		bloom_add(bloom, line);
	}

	fclose(fp);

	while(fgets(line, 1024, stdin)) {
		if((p=strchr(line, '\r'))) 
			*p='\0';
		if((p=strchr(line, '\n'))) 
			*p='\0';

		p=strtok(line, " \t,.;:\r\n?!-/()");
		while(p) {
			if(!bloom_check(bloom, p))
				printf("No match for word \"%s\"\n", p);
			else
				printf("Find word \"%s\"\n", p);
			p=strtok(NULL, " \t,.;:\r\n?!-/()");
		}
	}

	bloom_destroy(bloom);

	return EXIT_SUCCESS;
}



转载自:

1.http://www.cnblogs.com/heaad/archive/2011/01/02/1924195.html

2.http://blog.csdn.net/liuben/article/details/6602683

3.http://blog.redfox66.com/post/2010/09/24/mass-data-topic-2-bloom-filter.aspx

你可能感兴趣的:(数据结构,cache,filter,url,存储)