缓存场景:请求数据的时候,server 缓存 db
缓存穿透:redis, mysql 都没有数据存在,黑客可以利用此漏洞不断请求数据导致 mysql 压力过大,如此以来整个系统陷入瘫痪。
场景:用字符串去海量字符串库查询是否存在
set 和 map
-
C++ 标准库(STL) 中的 set 和 map 结构都是采用红黑树实现的,它的增删改查的时间复杂度是 O(logn);
-
k, v ,红黑树是有序的结构, key 是用来比较的字段;红黑树并没有要求 key 字段唯一,在 set 和 map 实现过程限制了 key 字段唯一
不限制唯一的关键源码
// 这个是截取 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 = 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
- C++ 中的 unordered_map 是采用的 hashtable 实现
- 构成:数组 + hash 函数
- 他是将字符串通过 hash 函数生成一个整数再映射到数组中;它的增删改查的时间复杂度是 O(1)
- hash 函数的作用:避免字符串的比较; hash 函数计算出来的值通过对数组长度的取模 随机分布 在数组当中;
- hash 函数一般返回的64位整数,将多个大叔映射到一个小数组中,必然回产生冲突
-
hash 冲突解决方案:
-
链表法
引入链表来处理哈希冲突;在极端情况下,如果冲突元素比较多,该冲突链表过长,这个时候可以将这个链表 (单独这个链表) 转换为 红黑树; 由原来链表时间复杂度 O(n) 转换为红黑树时间 O(logn);
-
开放寻址法
这两种都会导致 同类hash聚集; 也就是近似值它的 hash 值也近似,那么它的数组槽位也相近,形成
hash
聚集。另外可以用 双重哈希 解决上面出现的 hash 聚集现象
在
.net HashTable
类的 hash 函数定义如下:Hk(key) = [GetHash(key) + k * (1 + (((Gethash*(key) >> 5) + 1) & (hashsize - 1)))] % hashsize; 在 (1 + (((Gethash*(key) >> 5) + 1) & (hashsize - 1))) 与 hashsize 互为素数(两数互为素数表示两者额米有共同的质因子) hash + k * hash2 执行了 hashsize 次探查后,哈希表中的每一个位置都有且只有一次被访问到,也就是说,对于给定的 key, 对哈希表中的同一位置不会同时使用 Hi 和 Hj
-
-
同样,hashtable 中结点存储了 key 和 value,且没有要求 key 的大小顺序
-
优点:访问速度更快; 不需要进行字符串比较
-
缺点:需要引入策略避免冲突,存储效率不高;空间换时间
布隆过滤器
对于海量数据来说,上面两种结构都不适合做海量数据去重,内存不够大。所以引入了布隆过滤器
- 定义:布隆过滤器是一种概率性数据结构(包括跳表也是),特点是高效的 插入和查询, 能明确告知某个字符串 一定不存在或者可能存在
- 布隆过滤器相比传统的查询结构(如
hash, set, map
) 等查询结构,更加高效,占用空间更小;但是其缺点是它返回的结果是概率性的,也就是说 结果存在误差 的,虽然这个误差是可控的;同时他也不支持删除操作 - 组成:位图 (bit) + n 个 hash 函数
-
原理:当一个元素加入到位图时,通过k 个 hash 函数将元素映射到位图的 k 个点,并置为1;当检索时,再通过 k 个 hash 函数运算检测位图的 k 个点是否都为 1;如果有不为 1 的店,那么认为不存在;如果全为 1, 则可能存在 (存在误差)
-
在位图中每个槽位只有两种状态 0, 1, 一个槽位被设置为 1 状态,但不明确被设置了多少次,也就是不知道被多少个
str1
哈希映射以及被哪个hash
函数映射,所以不支持删除操作。 -
在实际应⽤过程中,布隆过滤器该如何使⽤?要选择多少个hash函数,要分配多少空间的位图,存
储多少元素?另外如何控制假阳率(布隆过滤器能明确⼀定不存在,不能明确⼀定存在,那么存在
的判断是有误差的,假阳率就是错误判断存在的概率)?
n -- 布隆过滤器中元素的个数,如上图 只有str1和str2 两个元素 那么 n=2
p -- 假阳率,在0-1之间
m -- 位图所占空间
k -- hash函数的个数
公式如下:
n
=
c
e
i
l
(
m
−
k
l
o
g
(
1
−
exp
(
l
o
g
(
p
)
k
)
)
)
p
=
(
1
−
exp
(
−
k
m
/
n
)
)
k
m
=
c
e
i
l
(
n
∗
l
o
g
(
p
)
l
o
g
(
1
2
l
o
g
2
)
)
k
=
r
o
u
n
d
(
m
n
∗
l
o
g
(
2
)
)
\begin{aligned} n &= ceil(\frac{m}{\frac{-k}{log(1-\exp(\frac{log(p)}{k}))}}) \\ p &= (1-\exp(\frac{-k}{m / n}))^k \\ m &= ceil(\frac{n * log(p)}{log(\frac{1}{2^{log2}})})\\ k &= round(\frac{m}{n}*log(2)) \end{aligned}
npmk=ceil(log(1−exp(klog(p)))−km)=(1−exp(m/n−k))k=ceil(log(2log21)n∗log(p))=round(nm∗log(2))
- 四个值的关系
- 已知 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++)
{
Pos[i] = (hash1 + i*hash2) % m;
}
// 通过这种⽅式来模拟 k 个hash函数