BLOOMFILTER
一、场景和问题
在高并发的环境中,会同时有大量的客户端向服务端请求数据,而当服务端接收到客户端的请求就会向数据库获取数据。为了更快的获取数据和降低数据库执行负载,通常会引入缓存进行优化。
缓存穿透是缓存的三大重要问题之一。它指的是缓存和数据库中同时都没有该数据,导致查询操作都会到达到数据库执行相关步骤,从而导致数据库系统的压力过大甚至崩溃。黑客会利用这种查询数据库没有的数据来攻击数据库,从而使整个系统瘫痪。
问题: 如何避免在数据库中执行查询不存在数据的操作。
二、解决方法
2.1 初步想法—set/map
在缓存(redis)端设置set/map数据集合来存储查询过但不存在的数据。当对缓存执行查询操作时即可根据集合判断是否存在,不存在就可直接返回,不进行数据库查询操作。
C++ STL中set/map数据结构的底层都是红黑树,它的查询效率是O(logN)。
优点:存储效率⾼,访问速度⾼效;
缺点:对于数据量⼤、查询数据比较复杂(如字符串)、且查询数据相似时将会是噩梦;
2.2 优化想法—unordered_set/unordered_map
在缓存(redis)端设置unordered_set/unordered_map数据集合来存储查询过但不存在的数据。
C++ STL中unordered_set/unordered_map的底层时hashtable,相比较set/map的优化在于它减少了一定的数据比较。减少了一定的比较是因为hashtable的存储原理可知,每个数据首先经过hash函数的映射得到存储位置,然后放入指定位置。如果无需向红黑树一样一个一个节点的比较判断。当然如果一个位置存放多个数据(使用链表进行链接,也可使用红黑树进行优化),查询数据还是需要和这些同个位置上的数据进行比较判断。
优点:访问速度更快;减少数据比较;
缺点:需要引⼊策略避免hash冲突,存储效率不⾼;空间换时间;
hash冲突解决⽅案:
1.链表法
引⼊链表来处理哈希冲突;也就是将冲突元素⽤链表链接起来;这也是常⽤的处理冲突的⽅式;但是可能出现⼀种极端情况,冲突元素⽐较多,该冲突链表过⻓,这个时候可以将这个链表转换为红⿊树;由原来链表时间复杂度 o(n) 转换为红⿊树时间复杂度 ;那么判断该链表过⻓的依据是多少?可以采⽤超过256(经验值)个节点的时候将链表结构转换为红⿊树结构;
2.开放寻址法
将所有的元素都存放在哈希表的数组中,不使⽤额外的数据结构;⼀般使⽤线性探查的思路解决;
a. 当插⼊新元素的时,使⽤哈希函数在哈希表中定位元素位置;
b. 检查数组中该槽位索引是否存在元素。如果该槽位为空,则插⼊,否则3;
c. 在 2 检测的槽位索引上加⼀定步⻓接着检查2;
加⼀定步⻓分为以下⼏种:
a. i+1, i+2, i+3, i+4 ... i+n
b. i-1^2, i+1^2, i-2^2, 1+2^2 ... i+n^2
这两种都会导致同类hash聚集;也就是近似值它的hash值也近似,那么它的数组槽位也靠近,形成hash聚集;第⼀种同类聚集冲突在前,第⼆种只是将聚集冲突延后。
总结:红⿊树和hashtable都不能解决海量数据问题,它们都需要存储具体数据,如果数据量⼤,提供不了⼏百G的内存;所以需要尝试探寻不存储key的⽅案,并且拥有hashtable的优点(不需要⽐较数据)。
2.3 进阶想法—BloomFilter
定义:布隆过滤器是⼀种概率型数据结构,它的特点是⾼效的插⼊和查询,能明确告知某个字符串一定不存在或者可能存在;
优点:布隆过滤器相⽐传统的查询结构(例如:hash,set,map等数据结构)更加⾼效,占⽤空间更⼩;
缺点:它返回的结果是概率性的,也就是说结果存在误差的,虽然这个误差是可控的;同时它不⽀持删除操作;
组成:位图(bit数组)+ n个hash函数
原理:当⼀个元素加⼊位图时,通过k个hash函数将这个元素映射到位图的k个点,并把它们置为1;当检索时,再通过k个hash函数运算检测位图的k个点是否都为1;如果有不为1的点,那么认为不存在;如果全部为1,则可能存在(存在误差);
问:为什么不支持删除操作?
答:在位图中每个槽位只有两种状态(0或者1),⼀个槽位被设置为1状态,但不明确它被设置了多少次;也就是不知道被多少个str1哈希映射以及是被哪个hash函数映射过来的;所以不⽀持删除操作。
三、简单使用
#include "bloomfilter.h"
#include <stdio.h>
#define MAX_ITEMS 4000 // 设置最大元素
#define ADD_ITEMS 1000 // 添加测试元素
#define P_ERROR 0.0000001 // 设置误差
int main(int argc, char** argv)
{
printf(" test bloomfilter\n");
// 1. 定义BaseBloomFilter
static BaseBloomFilter stBloomFilter = {0};
// 2. 初始化stBloomFilter,调用时传入hash种子,存储容量,以及允许的误判率
InitBloomFilter(&stBloomFilter, 0, MAX_ITEMS, P_ERROR);
// 3. 向BloomFilter中新增数值
char url[128] = {0};
for(int i = 0; i < ADD_ITEMS; i++){
sprintf(url, "https://0voice.com/%d.html", i);
if(0 == BloomFilter_Add(&stBloomFilter, (const void*)url, strlen(url))){
// printf("add %s success", url);
}else{
printf("add %s failed", url);
}
memset(url, 0, sizeof(url));
}
// 4. check url exist or not
const char* str = "https://0voice.com/0.html";
if (0 == BloomFilter_Check(&stBloomFilter, str, strlen(str)) ){
printf("https://0voice.com/0.html exist\n");
}
const char* str2 = "https://0voice.com/10001.html";
if (0 != BloomFilter_Check(&stBloomFilter, str2, strlen(str2)) ){
printf("https://0voice.com/10001.html not exist\n");
}
// 5. free bloomfilter
FreeBloomFilter(&stBloomFilter);
getchar();
return 0;
}