18.布隆过滤器的实现及应用
布隆过滤器介绍
布隆过滤器(Bloom Filter)由Burton Howard Bloom在1970年提出,它实际上是一个很长的二进制向量(位数组) 和一系列 随机映射函数(哈希)。布隆过滤器可以用于检索一个元素是否在一个集合中,是一种空间查询率高的概率型数据结构。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
它专门用来检测集合中是否存在特定的元素。在这里听起来很平常的需求,所以为什么要使用这种数据结构?
什么情况下需要用到布隆过滤器?
我们先来看几个比较常见的例子:
- 在字处理软件中,需要检查一个英语单词是否拼写正确;
- 网页爬虫对URL去重,避免爬取相同的URL地址;
- 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否是垃圾邮箱;
- 缓存穿透,将已经存在的缓存放在布隆中,当黑客访问不存在的缓存时迅速返回避免缓存及DB挂掉。
这几个例子有一个共同的特点:如何判断一个元素是否存在一个集合中?
常规思路:
- 数组
- 链表
- 树、平衡二叉树、Trie(字典树)
- Map(红黑树)
- 哈希表
虽然上面描述的这几种数据结构配合常见的排序、二分搜索可以快速高效的处理绝大部分判断元素是否存在集合中的需求。但是当集合里面的元素数量足够大,如果有500W条记录甚至1亿条记录呢?这个时候常规的数据结构的问题就凸显出来了。数组、链表、树等数据结构回存储元素的内容,一旦数据量过大,消耗的内存约会呈现线性增长,最终达到瓶颈。
有的同学可能回问,哈希表不是效率很高吗?查询效率可以达到O(1)。但是哈希表需要消耗的内存依然很高。使用哈希表存储1亿个垃圾email地址的消耗?哈希表的做法:首先,哈希函数将一个email地址映射成8字节信息指纹;考虑到哈希表存储效率通常小于50%(哈希冲突);因此消耗的内存:8 * 2 * 1亿 字节 = 1.6G内存,普查计算机是无法提供如此大的内存。这个时候,布隆过滤器(Bloom Filter)就应运而生。
在理解布隆过滤器的原理时,先来回顾一下哈希函数的知识。
哈希函数
哈希函数的概念是:将任意大小的数据转换成特定大小的数据的函数,转换后的数据称为哈希值或哈希编码。
对于哈希表来说它不仅有哈希函数来得到这么一个index
值,且它会把整个要存的元素全部都放到哈希表里面取,这是一个没有误差的数据结构且有多少的元素,每个元素有多大,那么所有的这些元素需要占的内存空间在哈希表里面都要找相应的内存的大小给存进来。
那么在很多时候我们在工业级应用的时候,发现我们并不需要存所有元素本身,而只需要存一个信息,就是说这个元素在我这个表里面到底是有还是没有。在这种情况下如果只要查询有还是没有的时候,这个时候我们就需要一种更高效的数据结构,更高效的数据结构可以导致的一个结果,就这里有很多元素要存的话,但是我们这个表的话所需要的内存空间很少,同时的话我们不需要把元素本身全部存起来,我们只需要说把这个东西到底是有还是没有。接下来我们看这个数据结构到底是怎么设计出来的。
布隆过滤器原理
布隆过滤器(Bloom Filter)的核心实现是一个超大的位数组和几个哈希函数。假设位数组的长度为 m
,哈希函数的个数为k
。
具体的操作流程:假设集合里面有3个元素 {x,y,z}
, 哈希函数的个数为3。
- 首先将位数组进行初始化,将里面每个位都设置为0。
- 对于集合里面的每个元素,将元素依次通过3个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记位1。
- 查询
W
元素是否存在集合中的时候,同样的方法将W通过哈希表映射到位数组上的3个点。如果3个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。反之,如果3个点都为1,则该元素可能存在集合中。
注意:此处不能判断该元素是否一定存在集合中,可能存在一定的误判率。可以从图中看到:假设某个元素通过映射对应下标为4,5,6这3个点。虽然这3个点都为1,但是很明显这3个点是不同元素经过哈希表得到的位置,因此这种情况说明元素虽然不在集合中,也可能对应的都是1,这就是误判率存在的原因。
布隆过滤器的设计
布隆过滤器思路比较简单,但是对于布隆过滤器的随机映射函数设计,需要计算几次,向量长度设置多少比较合适,这个才是需要认真讨论的。
- 如果向量长度太短,会导致误判率直线上升。
- 如果向量太长,会浪费大量内存。
- 如果计算次数过多,会占用计算资源,且很容易很快就把过滤器填满。
布隆过滤器的简单实现
布隆过滤器添加元素
- 将要添加的元素给k个哈希函数
- 得到对应于位数组上的k个位置
- 将这k个位置设为1
布隆过滤器查询元素
- 将要查询的元素给k个哈希函数
- 得到对应于位数组上的k个位置
- 如果k个位置有一个为0,则肯定不在集合中
- 如果k个位置全部为1,则可能在集合中
java实现
//Java
public class BloomFilter {
private static final int DEFAULT_SIZE = 2 << 24;
private static final int[] seeds = new int[] { 5, 7, 11, 13, 31, 37, 61 };
private BitSet bits = new BitSet(DEFAULT_SIZE);
private SimpleHash[] func = new SimpleHash[seeds.length];
public BloomFilter() {
for (int i = 0; i < seeds.length; i++) {
func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]);
}
}
public void add(String value) {
for (SimpleHash f : func) {
bits.set(f.hash(value), true);
}
}
public boolean contains(String value) {
if (value == null) {
return false;
}
boolean ret = true;
for (SimpleHash f : func) {
ret = ret && bits.get(f.hash(value));
}
return ret;
}
// 内部类,simpleHash
public static class SimpleHash {
private int cap;
private int seed;
public SimpleHash(int cap, int seed) {
this.cap = cap;
this.seed = seed;
}
public int hash(String value) {
int result = 0;
int len = value.length();
for (int i = 0; i < len; i++) {
result = seed * result + value.charAt(i);
}
return (cap - 1) & result;
}
}
}
Python实现
# Python
from bitarray import bitarray
import mmh3
class BloomFilter:
def __init__(self, size, hash_num):
self.size = size
self.hash_num = hash_num
self.bit_array = bitarray(size)
self.bit_array.setall(0)
def add(self, s):
for seed in range(self.hash_num):
result = mmh3.hash(s, seed) % self.size
self.bit_array[result] = 1
def lookup(self, s):
for seed in range(self.hash_num):
result = mmh3.hash(s, seed) % self.size
if self.bit_array[result] == 0:
return "Nope"
return "Probably"
bf = BloomFilter(500000, 7)
bf.add("dantezhao")
print (bf.lookup("dantezhao"))
print (bf.lookup("yyj"))
C/C++实现
C/C++
#include <iostream>
#include <bitset>
#include <cmath>
using namespace std;
typedef unsigned int uint;
const int DEFAULT_SIZE = 1 << 20;
const int seed[] = { 5, 7, 11, 13, 31, 37, 61 };
class BloomFilter {
public:
BloomFilter() : hash_func_count(3) {}
BloomFilter(int bitsize, int str_count) {
hash_func_count = ceil((bitsize / str_count) * log(2));
}
~BloomFilter() {}
uint RSHash(const char *str, int seed);
void Add(const char *str);
bool LookUp(const char *str);
private:
int hash_func_count;
bitset<DEFAULT_SIZE> bits;
};
uint BloomFilter::RSHash(const char *str, int seed) {
uint base = 63689;
uint hash = 0;
while (*str) {
hash = hash * base + (*str++);
base *= seed;
}
return (hash & 0x7FFFFFFF);
}
void BloomFilter::Add(const char* str) {
int index = 0;
for(int i = 0; i < hash_func_count; ++i) {
index = static_cast<int>(RSHash(str, seed[i])) % DEFAULT_SIZE;
bits[index] = 1;
}
return ;
}
bool BloomFilter::LookUp(const char* str) {
int index = 0;
for(int i = 0; i < hash_func_count; ++i) {
index = static_cast<int>(RSHash(str, seed[i])) % DEFAULT_SIZE;
if (!bits[index]) return false;
}
return true;
}
javascript实现
// JavaScript
class BloomFilter {
constructor(maxKeys, errorRate) {
this.bitMap = [];
this.maxKeys = maxKeys;
this.errorRate = errorRate;
// 位图变量的长度,需要根据maxKeys和errorRate来计算
this.bitSize = Math.ceil(maxKeys * (-Math.log(errorRate) / (Math.log(2) * Math.log(2))));
// 哈希数量
this.hashCount = Math.ceil(Math.log(2) * (this.bitSize / maxKeys));
// 已加入元素数量
this.keyCount = 0;
}
bitSet(bit) {
let numArr = Math.floor(bit / 31);
let numBit = Math.floor(bit % 31);
this.bitMap[numArr] |= 1 << numBit;
}
bitGet(bit) {
let numArr = Math.floor(bit / 31);
let numBit = Math.floor(bit % 31);
return (this.bitMap[numArr] &= 1 << numBit);
}
add(key) {
if (this.contain(key)) {
return -1;
}
let hash1 = MurmurHash(key, 0, 0),
hash2 = MurmurHash(key, 0, hash1);
for (let i = 0; i < this.hashCount; i++) {
this.bitSet(Math.abs(Math.floor((hash1 + i * hash2) % this.bitSize)));
}
this.keyCount++;
}
contain(key) {
let hash1 = MurmurHash(key, 0, 0);
let hash2 = MurmurHash(key, 0, hash1);
for (let i = 0; i < this.hashCount; i++) {
if (!this.bitGet(Math.abs(Math.floor((hash1 + i * hash2) % this.bitSize)))) {
return false;
}
}
return true;
}
}
/**
* MurmurHash
*
* 参考 http://murmurhash.googlepages.com/
*
* data:待哈希的值
* offset:
* seed:种子集
*
*/
function MurmurHash(data, offset, seed) {
let len = data.length,
m = 0x5bd1e995,
r = 24,
h = seed ^ len,
len_4 = len >> 2;
for (let i = 0; i < len_4; i++) {
let i_4 = (i << 2) + offset,
k = data[i_4 + 3];
k = k << 8;
k = k | (data[i_4 + 2] & 0xff);
k = k << 8;
k = k | (data[i_4 + 1] & 0xff);
k = k << 8;
k = k | (data[i_4 + 0] & 0xff);
k *= m;
k ^= k >>> r;
k *= m;
h *= m;
h ^= k;
}
// avoid calculating modulo
let len_m = len_4 << 2,
left = len - len_m,
i_m = len_m + offset;
if (left != 0) {
if (left >= 3) {
h ^= data[i_m + 2] << 16;
}
if (left >= 2) {
h ^= data[i_m + 1] << 8;
}
if (left >= 1) {
h ^= data[i_m];
}
h *= m;
}
h ^= h >>> 13;
h *= m;
h ^= h >>> 15;
return h;
}
let bloomFilter = new BloomFilter(10000, 0.01);
bloomFilter.add("abcdefgh");
console.log(bloomFilter.contain("abcdefgh"));
console.log(bloomFilter.contain("abcdefghi"));
附:其它实现
常见案例
- 比特币网络
- 分布式系统(Map-Reduce) — Hadoop、search engine
- Redis 缓存
- 垃圾邮件、评论等的过滤
参考文章:使用布隆过滤器解决缓存击穿、垃圾邮件识别、集合判重
部分图片来源于网络,版权归原作者,侵删。