目录
[3.1 Set](#3.1 Set)
[3.2 Test](#3.2 Test)
1、布隆过滤器的概念
有⼀些场景下⾯,有⼤量数据需要判断是否存在,⽽这些数据不是整形,那么位图就不能使⽤了,使⽤红⿊树/哈希表等内存空间可能不够。这些场景就需要布隆过滤器来解决。
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 ⼀种紧凑型的、⽐较巧妙的概率型数据结构,特点是⾼效地插⼊和查询,可以⽤来告诉你 "某样东西⼀定不存在或者可能存在",它是⽤多个哈希函数,将⼀个数据映射到位图结构中。此种⽅式不仅可以提升查询效率,也可以节省⼤量的内存空间。
布隆过滤器的思路就是把key先映射转成哈希整型值,再映射⼀个位,如果只映射⼀个位的话,冲突率会⽐较多,所以可以通过多个哈希函数映射多个位,降低冲突率 。
布隆过滤器这⾥跟哈希表不⼀样,它⽆法解决哈希冲突的,因为他压根就不存储这个值,只标记映射的位。它的思路是尽可能降低哈希冲突。判断⼀个值key在是不准确的,判断⼀个值key不在是准确的。
像这幅图中一个数据映射了3个位,只有当3个位都是1时,才认为这个数据是存在的,注意此时仍然有可能是不存在的,即是存在误判的,这里的猪八戒就是存在的。当3个位置中有一个位置是0时,这个数据就是不存在的,这是一定的,这里的孙悟空就是不存在的。
2、布隆过滤器误判率推导
这里是利用数学就行推导
3、代码实现
布隆过滤器底层使用的是上一节的位图,不过这里需要增加仿函数将其他类型映射为整型
struct HashFuncBKDR
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto ch : s)
{
hash *= 31;
hash += ch;
}
return hash;
}
};
struct HashFuncAP
{
// 由Arash Partow发明的⼀种hash算法。
size_t operator()(const string& s)
{
size_t hash = 0;
for (size_t i = 0; i < s.size(); i++)
{
if ((i & 1) == 0) // 偶数位字符
{
hash ^= ((hash << 7) ^ (s[i]) ^ (hash >> 3));
}
else // 奇数位字符
{
hash ^= (~((hash << 11) ^ (s[i]) ^ (hash >>
5)));
}
}
return hash;
}
};
struct HashFuncDJB
{
// 由Daniel J. Bernstein教授发明的⼀种hash算法。
size_t operator()(const string& s)
{
size_t hash = 5381;
for (auto ch : s)
{
hash = hash * 33 ^ ch;
}
return hash;
}
};
template<size_t N,
size_t X = 5,
class K = std::string,
class Hash1 = HashFuncBKDR,
class Hash2 = HashFuncAP,
class Hash3 = HashFuncDJB>
class BloomFilter
{
public:
private:
static const size_t M = N * X;
cxf::bitset<M> _bs;
};
这里设计3个哈希函数的布隆过滤器。模板参数N是布隆过滤器开的位数,X是误导率推导中的m / n,根据前面的计算,当哈希函数是3个时,m / n是5则误导率最低的,K是数据的类型,默认是string,后面3个参数是仿函数,将string映射为整型,3个仿函数对同一个string映射的结果需要不同。注意:这里的N = n,X = m / n,所以虽然传过来需要开一个N位的布隆过滤器,但是实际上的位图需要开N * X个位的位图
3.1 Set
将映射到的位置全部置为1
void Set(const K& key)
{
// 计算在不同仿函数下映射到的位置
size_t hash1 = Hash1()(key) % M;
size_t hash2 = Hash2()(key) % M;
size_t hash3 = Hash3()(key) % M;
// 将映射到的位置设置为1
_bs.set(hash1);
_bs.set(hash2);
_bs.set(hash3);
}
3.2 Test
检查一个值是否在布隆过滤器中。此时需要检查映射到的位置是否全为1,若全为1,在,此时可能误判,若有1个或1个以上位置不为1,则不在,此时没有误判
bool Test(const K& key)
{
size_t hash1 = Hash1()(key) % M;
if (!_bs.test(hash1)) // 如果映射到的这个位置是0,就返回false,此时没有误判
return false;
size_t hash2 = Hash2()(key) % M;
if (!_bs.test(hash2)) // 如果映射到的这个位置是0,就返回false,此时没有误判
return false;
size_t hash3 = Hash3()(key) % M;
if (!_bs.test(hash3)) // 如果映射到的这个位置是0,就返回false,此时没有误判
return false;
return true; // 映射到的位置全为1,则返回true,此时可能存在误判
}
此时可以就行测试
void TestBloomFilter()
{
BloomFilter<10> bf;
bf.Set("猪八戒");
bf.Set("孙悟空");
bf.Set("唐僧");
cout << bf.Test("猪八戒") << endl;
cout << bf.Test("孙悟空") << endl;
cout << bf.Test("唐僧") << endl;
cout << bf.Test("沙僧") << endl;
cout << bf.Test("猪八戒1") << endl;
cout << bf.Test("猪戒八") << endl;
}
结果是1 1 1 0 0 0,是没有问题的
struct HashFuncBKDR
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto ch : s)
{
hash *= 31;
hash += ch;
}
return hash;
}
};
struct HashFuncAP
{
// 由Arash Partow发明的⼀种hash算法。
size_t operator()(const string& s)
{
size_t hash = 0;
for (size_t i = 0; i < s.size(); i++)
{
if ((i & 1) == 0) // 偶数位字符
{
hash ^= ((hash << 7) ^ (s[i]) ^ (hash >> 3));
}
else // 奇数位字符
{
hash ^= (~((hash << 11) ^ (s[i]) ^ (hash >>
5)));
}
}
return hash;
}
};
struct HashFuncDJB
{
// 由Daniel J. Bernstein教授发明的⼀种hash算法。
size_t operator()(const string& s)
{
size_t hash = 5381;
for (auto ch : s)
{
hash = hash * 33 ^ ch;
}
return hash;
}
};
template<size_t N,
size_t X = 5,
class K = std::string,
class Hash1 = HashFuncBKDR,
class Hash2 = HashFuncAP,
class Hash3 = HashFuncDJB>
class BloomFilter
{
public:
void Set(const K& key)
{
// 计算在不同仿函数下映射到的位置
size_t hash1 = Hash1()(key) % M;
size_t hash2 = Hash2()(key) % M;
size_t hash3 = Hash3()(key) % M;
// 将映射到的位置设置为1
_bs.set(hash1);
_bs.set(hash2);
_bs.set(hash3);
}
bool Test(const K& key)
{
size_t hash1 = Hash1()(key) % M;
if (!_bs.test(hash1)) // 如果映射到的这个位置是0,就返回false,此时没有误判
return false;
size_t hash2 = Hash2()(key) % M;
if (!_bs.test(hash2)) // 如果映射到的这个位置是0,就返回false,此时没有误判
return false;
size_t hash3 = Hash3()(key) % M;
if (!_bs.test(hash3)) // 如果映射到的这个位置是0,就返回false,此时没有误判
return false;
return true; // 映射到的位置全为1,则返回true,此时可能存在误判
}
private:
static const size_t M = N * X;
cxf::bitset<M> _bs;
};
4、布隆过滤器的删除
布隆过滤器默认情况下是不支持删除的
在上面这幅图中,猪八戒和孙悟空都在布隆过滤器中,如果此时将猪八戒删除,也就是将猪八戒映射到的3个位都置成0,此时孙悟空也被删除了
解决⽅案:可以考虑计数标记的⽅式,⼀个位置用多个位标记,记录映射这个位的计数值,删除时,仅仅减减计数,那么就可以某种程度⽀持删除。但是这个⽅案也有缺陷,如果⼀个值不在布隆过滤器中(Test返回结果是真,也就是出现了误判),我们去删除,减减了映射位的计数,那么会影响已存在的值,也就是说,⼀个确定存在的值,可能会变成不存在,这⾥就很坑。当然也有⼈提出,我们可以考虑计数⽅式⽀持删除,但是定期重建⼀下布隆过滤器,这样也是⼀种思路。
5、布隆过滤器的应用
首先,我们先来分析一下布隆过滤器的优缺点
优点:效率⾼,节省空间,相⽐位图,可以适⽤于各种类型的标记过滤
缺点:存在误判(在是不准确的,不在是准确的),不好⽀持删除
布隆过滤器的一些应用
爬⾍系统中URL去重:
在爬⾍系统中,为了避免重复爬取相同的URL,可以使⽤布隆过滤器来进⾏URL去重。爬取到的URL可以通过布隆过滤器进⾏判断,已经存在的URL则可以直接忽略,避免重复的⽹络请求和数据处理。
垃圾邮件过滤:
在垃圾邮件过滤系统中,布隆过滤器可以⽤来判断邮件是否是垃圾邮件。系统可以将已知的垃圾邮件的特征信息存储在布隆过滤器中,当新的邮件到达时,可以通过布隆过滤器快速判断是否为垃圾邮件,从⽽提⾼过滤的效率。
预防缓存穿透:
在分布式缓存系统中,布隆过滤器可以⽤来解决缓存穿透的问题。缓存穿透是指恶意⽤⼾请求⼀个不存在的数据,导致请求直接访问数据库,造成数据库压⼒过⼤。布隆过滤器可以先判断请求的数据是否存在于布隆过滤器中,如果不存在,直接返回不存在,避免对数据库的⽆效查询。
对数据库查询提效:
在数据库中,布隆过滤器可以⽤来加速查询操作。例如:⼀个app要快速判断⼀个电话号码是否注册过,可以使⽤布隆过滤器来判断⼀个⽤⼾电话号码是否存在于表中,如果不存在,可以直接返回不存在,避免对数据库进⾏⽆⽤的查询操作。如果在,再去数据库查询进⾏⼆次确认。