学习本文章的小提示:如果你已经有了一定基础,可以直接看文章的黑体总结部分。
不看真会后悔
在开始之前我们先思考以下几个问题 | |
---|---|
1.在使⽤word⽂档时,word如何判断某个单词是否拼写正确? | |
2.⽹络爬⾍程序,怎么让它不去爬相同的url⻚⾯? | |
3.垃圾邮件(短信)过滤算法如何设计? | |
4.公安办案时,如何判断某嫌疑⼈是否在⽹逃名单中? | |
5.缓存穿透问题如何解决? |
对于前四个问题,这些大量的数据我们应该如何进行存储判断
如果数据量比较少,我们可以直接挨个与数据库存储的信息进行对比得到答案,但如果数据量十分庞大,甚至达到上亿的级别,我们还能直接与数据库的信息进行对比吗?
想要明白上一点,我们先来聊聊关于缓存穿透的问题,它是如何发生的,以及我们该如何解决它。
当server端向数据库请求数据时,中间缓存组件(redis)和数据库(此处是MySQL)都不存在相应数据,此时如果请求量较大,由于中间缓冲组件(redis)没有对应的缓存数据,于是压力全部给到数据库上,导致压力过大出现系统瘫痪的情况被称之为缓存穿透。
需求:
从海量数据中查询某字符串是否存在。
学过C++的朋友都知道set和map,但你清除它们的内部是如何实现的吗?
set和map
c++标准库(STL)中的set和map结构都是采⽤红⿊树实现的,它增删改查的时间复杂度是O(log2 N);
图结构示例:
对于严格平衡⼆叉搜索树(AVL),100w条数据组成的红⿊树,只需要⽐较20次就能找到该值;对于10亿条数据只需要⽐较30次就能找到该数据;也就是查找次数跟树的⾼度是⼀致的;
对于红⿊树来说平衡的是⿊节点⾼度,所以研究⽐较次数需要考虑树的⾼度差,最好情况某条树链路全是⿊节点,假设此时⾼度为h1,最差情况某条树链路全是⿊红节点间隔,那么此时树⾼度为2*h1;
在红⿊树中每⼀个节点都存储key和val字段,key是⽤来做⽐较的字段;红⿊树并没有要求key字段唯⼀,在set和map实现过程中限制了key字段唯⼀。我们来看nginx的红⿊树实现:
// 这个是截取 nginx 的红⿊树的实现,这段代码是 insert 操作中的⼀部分,执⾏完这个函数还需要检测插⼊节点后是否平衡(主要是看他的⽗节点是否也是红⾊节点)
// 调⽤ ngx_rbtree_insert_value 时,temp传的参数为 红⿊树的根节点,node传的参数为待插⼊的节点
void ngx_rbtree_insert_value(ngx_rbtree_node_t *temp, ngx_rbtree_node_t
*node,
ngx_rbtree_node_t *sentinel)
{
ngx_rbtree_node_t **p;
for ( ;; ) {
p = (node->key < temp->key) ? &temp->left : &temp->right;// 这⾏很重要
if (*p == sentinel) {
break;
}
temp = *p;
}
*p = node;
node->parent = temp;
node->left = sentinel;
node->right = sentinel;
ngx_rbt_red(node);
}
// 不插⼊相同节点 如果插⼊相同 让它变成修改操作 此时 红⿊树当中就不会有相同的key了
定时器 key 时间戳
// 如果我们插⼊key = 12,如上图红⿊树,12号节点应该在哪个位置? 如果我们要实现插⼊存在的节点变成修改操作,该怎么改上⾯的函数
void ngx_rbtree_insert_value_ex(ngx_rbtree_node_t *temp, ngx_rbtree_node_t
*node, ngx_rbtree_node_t *sentinel) {
ngx_rbtree_node_t **p;
for ( ;; ) {
// {-------------add-------------
if (node->key == temp->key) {
temp->value = node->value;
return;
}
// }-------------add-------------
p = (node->key < temp->key) ? &temp->left : &temp->right;// 这⾏很重要
if (*p == sentinel) {
break;
}
temp = *p;
}
*p = node;
node->parent = temp;
node->left = sentinel;
node->right = sentinel;
ngx_rbt_red(node);
}
另外set和map的关键区别是set不存储val字段;
优点:存储效率⾼,访问速度⾼效;
缺点:对于数据量⼤且查询字符串⽐较⻓且查询字符串相似时将会是噩梦;
unordered_map
map内部是红黑树实现的,unordered_map则采用了hashtable
c++标准库(STL)中的unordered_map<string, bool>是采⽤hashtable实现的;
构成:数组+hash函数;
它是将字符串通过hash函数⽣成⼀个整数再映射到数组当中;它增删改查的时间复杂度是o(1);
图结构示例:
hash函数的作⽤:避免插⼊的时候字符串的⽐较;hash函数计算出来的值通过对数组⻓度的取模
能随机分布在数组当中;
hash函数⼀般返回的是64位整数,将多个⼤数映射到⼀个⼩数组中,必然会产⽣冲突;
如何选取hash函数?
https://github.com/aappleby/smhasher
这两种都会导致同类hash聚集;也就是近似值它的hash值也近似,那么它的数组槽位也靠近,形成hash聚集;第⼀种同类聚集冲突在前,第⼆种只是将聚集冲突延后;
另外还可以使⽤双重哈希来解决上⾯出现hash聚集现象:
具体原理请点击:双重哈希原理https://www.cnblogs.com/organic/p/6283476.html
同样的hashtable中节点存储了key和val,hashtable并没有要求key的⼤⼩顺序,我们同样可以修改代码让插⼊存在的数据变成修改操作;
优点:访问速度更快;不需要进⾏字符串⽐较;
缺点:需要引⼊策略避免冲突,存储效率不⾼;空间换时间;
定义:布隆过滤器是⼀种概率型数据结构,它的特点是⾼效的插⼊和查询,能明确告知某个字符串⼀定不存在或者可能存在
布隆过滤器相⽐传统的查询结构(例如:hash,set,map等数据结构)更加⾼效,占⽤空间更⼩;但是其缺点是它返回的结果是概率性的,也就是说结果存在误差的,虽然这个误差是可控的;
同时它不⽀持删除操作(至于为何不支持,继续往后看)
组成:位图(bit数组)+ n个hash函数
原理:当⼀个元素加⼊位图时,通过k个hash函数将这个元素映射到位图的k个点,并把它们置为1;当检索时,再通过k个hash函数运算检测位图的k个点是否都为1;如果有不为1的点,那么认为不存在;如果全部为1,则可能存在(存在误差);
假定我们选取这四个值为:
n = 4000
p = 0.000000001
m = 172532
k = 30
在实际应⽤中,我们确定n和p,通过上⾯的计算算出m和k;也可以在⽹站上选取合适的值:计算m和k的网站(点我https://hur.st/bloomfilter)
已知k,如何选择k个hash函数?
// 采⽤⼀个hash函数,给hash传不同的种⼦偏移值
// #define MIX_UINT64(v) ((uint32_t)((v>>32)^(v)))
uint64_t hash1 = MurmurHash2_x64(key, len, Seed);
uint64_t hash2 = MurmurHash2_x64(key, len, MIX_UINT64(hash1));
for (i = 0; i < k; i++) // k 是hash函数的个数
{
Pos[i] = (hash1 + i*hash2) % m; // m 是位图的⼤⼩
}
// 通过这种⽅式来模拟 k 个hash函数 跟我们前⾯开放寻址法 双重hash是⼀样的思路
应⽤源码http://gitlab.0voice.com/0voice/bloomfilter/tree/master: