海量数据去重的Hash、bitmap、BloomFilter、分布式一致性hash

总体知识脉络

在这里插入图片描述

背景

  • 使用 word 文档时,word 如何判断某个单词是否拼写正确?
  • 网络爬虫程序,怎么让它不去爬相同的 url 页面?
  • 垃圾邮件过滤算法如何设计?
  • 公安办案时,如何判断某嫌疑人是否在网逃名单中?
  • 缓存穿透问题如何解决?

需求

  从海量数据中查询某字符串是否存在

平衡二叉树

在这里插入图片描述

  不论是AVL还是红黑树,在“海量数据”数据面前都是不合适的,因为红黑树会将key,即数据存储起来,而海量的数据会导致内存不足。并且设计到字符串比较,效率也是很慢的。所以在这个需求下,用树相关的数据结构是不合适的。
 
扩展:

  • c++标准库(STL)中的set和map结构都是采⽤红⿊树实现的,它增删改查的时间复杂度是O(log2N)。set和map的关键区别是set不存储val字段。
  • 优点:存储效率⾼,访问速度⾼效
  • 缺点:对于数据量⼤且查询字符串⽐较⻓且查询字符串相似时将会是噩梦

散列表hashtable

  散列表构成:数组+hash函数。它是将字符串通过hash函数⽣成⼀个整数再映射到数组当中(所以散列表不需要”比较字符串“,而红黑树需要),它增删改查的时间复杂度是o(1)。
 
注意:散列表的节点中 kv 是存储在一起的

struct node {
	void *key;
	void *val;
	struct node *next;
};

扩展:

  • c++标准库(STL)中的unordered_map<string, bool>是采⽤hashtable实现的
  • hashtable中节点存储了key和val,hashtable并没有要求key的⼤⼩顺序,我们同样可以修改代码让插⼊存在的数据变成修改操作
  • 优点:访问速度更快;不需要进⾏字符串⽐较
  • 缺点:需要引⼊策略避免冲突,存储效率不⾼;空间换时间

hash函数

  hash函数:映射函数 Hash(key)=addr ;hash 函数可能会把两个或两个以上的不同 key 映射到同一地址,这种情况称之为冲突(或者hash 碰撞);
  hash函数的作用:避免插⼊的时候字符串的⽐较,hash函数计算出来的值通过对数组⻓度的取模能随机分布在数组当中。

选择hash

如何选取hash函数?

  • 选取计算速度快
  • 强随机分布(等概率、均匀地分布在整个地址空间)
      murmurhash1,murmurhash2,murmurhash3,siphash(redis6.0当中使⽤,rust等大多数语言选用的hash算法来实现hashmap),cityhash 都具备强随机分布性。siphash 主要解决字符串接近的强随机分布性,所以如果要hash字符串的话,优先选用siphash。

负载因子

  用来形容散列表的存储密度。数组存储元素的个数 / 数据长度;负载因子越小,冲突越小,负载因子越大,冲突越大。

冲突处理

  • 拉链法
      引用链表来处理哈希冲突,也就是将冲突元素用链表链接起来,这也是常用的处理冲突的⽅式。但是可能出现一种极端情况,冲突元素比较多,该冲突链表过长,这个时候可以将这个链表转换为红黑树。由原来链表时间复杂度 转换为红黑树时间复杂度 ,那么判断该链表过长的依据是多少?可以采⽤超过 256(经验值)个节点的时候将链表结构转换为红黑树结构。
    在这里插入图片描述

  • 开放地址法
      将所有的元素都存放在哈希表的数组中,不使用额外的数据结构;一般使用线性探查的思路解决,具体步骤如下:
      1. 当插⼊新元素的时,使⽤哈希函数在哈希表中定位元素位置
      2. 检查数组中该槽位索引是否存在元素。如果该槽位为空,则插⼊,否则3
      3. 在 2 检测的槽位索引上加⼀定步⻓接着检查2
       加⼀定步⻓分为以下几种:
        1. i+1,i+2,i+3,i+4, … ,i+n
        2. 在这里插入图片描述
        这两种都会导致同类hash聚集,也就是近似值它的hash值也近似。那么它的数组槽位也靠近,形成hash聚集。第⼀种同类聚集冲突在前,第⼆种只是将聚集冲突延后。
        3. 可以使⽤双重哈希来解决上⾯出现hash聚集现象

在.net HashTable类的hash函数Hk定义如下:
Hk(key) = [GetHash(key) + k * (1 + (((GetHash(key) >> 5) + 1) % (hashsize – 1)))] % hashsize
在此 (1 + (((GetHash(key) >> 5) + 1) % (hashsize – 1))) 与 hashsize 互为素数(两数互为素数表示两者没有共同的质因⼦)
执⾏了 hashsize 次探查后,哈希表中的每⼀个位置都有且只有⼀次被访问到,也就是说,对于给定的 key,对哈希表中的同⼀位置不会同时使⽤ Hi 和 Hj;
具体原理:https://www.cnblogs.com/organic/p/6283476.html

Stl中unordered_*散列表实现

  在 STL 中 unordered_map 、 unordered_set 、unordered_multimap 、 unordered_multiset 四兄弟底层实现都是散列表;
在这里插入图片描述
说明:因为STL中需要实现迭代器的原因,所以散列表中所有元素通过链表连在了一起。

关于hashtable中桶的数量相关的说明

  hashtable中桶的个数最好为质数,并且在此基础上最好为奇数,这样能够保证数据能够更均匀的分布键到桶中,具体原因可以参考链接:hashtable中桶的数量为什么最好选质数

散列表总结

在这里插入图片描述

bitmap

  先来介绍一下bitmap,下面再引出布隆过滤器。现在有一个需求:文件中有40亿个QQ号码,请设计算法对QQ号码去重,相同的QQ号码仅保留一个,内存限制1G。

  • 如果先排序再去重,时间复杂度太高
  • 如果用hashmap天然去重,空间复杂度太高
  • 文件切割避免内存过大,太麻烦,效率不高
  • 使用bitmap,可以顺利地同时解决时间问题和空间问题
     
      一个unsigned int类型,共有32位,可以标识0 ~ 31这32个整数的存在与否。两个unsigned int类型,共有64位,可以标识0 ~ 63这64个整数的存在与否。
     
      那么如果我们把整个整数范围都覆盖了,这样一来1代表第一个位,2代表第二个位,2的32次方代表最后一个位。40亿个数中,存在的数就在相应的位置1,其他位就是0。比如来了一个1234,就找一下第1234位,如果是1就存在,是0就不存在。
     
      所以我们只要有足够的”位“,就可以判断0~4亿这4亿个整数是否存在了。2的32次是4,294,967,296。也就是说我们要有4,294,967,296个位。
     
       4,294,967,296个位=2的32次方个位=2的29次方个字节=512MB。原来32位的整数,转化成了1位的布尔,所以数据空间就是原来的32分之一。
     
      可以看到,使用bitmap不但自动去重,甚至这个需求我们还排了序,从小到大遍历正整数,当bitmap位的值为1时,就输出该值,输出后的正整数序列就是排序后的结果。
     
    在这里插入图片描述

在这里插入图片描述

海量数据布隆过滤器

  红⿊树和hashtable都不能解决海量数据问题,它们都需要存储具体字符串,如果数据量⼤,提供不了⼏百G的内存;所以需要尝试探寻不存储key的⽅案,并且拥有hashtable的优点(不需要⽐较字符串)。而布隆过滤器就刚好满足这一需求,它不需要存储具体字符串,也不需要比较。时间和空间复杂度都低。

介绍

  布隆过滤器是一种概率型数据结构,它的特点是高效地插入和
查询,能确定某个字符串一定不存在或者可能存在
  优缺点:布隆过滤器相⽐传统的查询结构(例如:hash,set,map等数据结构)更加⾼效,占⽤空间更⼩,但是确定是它返回的结果是概率性的,结果存在一定的误差,误差可控,同时不支持删除操作
  构成:位图(bit数组)+ n个hash函数。

原理

  当一个元素加入位图时,通过 k 个 hash 函数将这个元素映射到位图的 k 个点,并把它们置为 1;
  当检索时,再通过 k 个 hash函数运算检测位图的 k 个点是否都为 1;如果有不为 1 的点,那么认为该 key 不存在;如果全部为 1,则可能存在;
 

在这里插入图片描述
  为什么不支持删除操作?
  在位图中每个槽位只有两种状态(0 或者 1),一个槽位被设置为 1 状态,但不确定它被设置了多少次;也就是不知道被多少个 key 哈希映射而来以及是被具体哪个 hash 函数映射而来;

  如果想实现删除操作,可以用两个布隆过滤器,将删除的元素,放入第二个布隆过滤器里面,然后查询的时候去第二个里面查,如果第二个里面能查到说明可能被删除(注意也是存在误差的)。

布隆过滤器应用分析

  在实际应用中,该选择多少个 hash 函数?要分配多少空间的位图?预期存储多少元素?如何控制误差?

变量关系

n ---- 预期布隆过滤器中元素的个数,如上图 只有str1和str2 两个元素 那么 n=2
p ---- 假阳率,在0-1之间 0.000000
m ---- 位图所占空间
k ----- hash函数的个数

公式如下:
n = ceil(m / (-k / log(1 - exp(log(p) / k))))
p = pow(1 - exp(-k / (m / n)), k)
m = ceil((n * log(p)) / log(1 / pow(2, log(2))))
k = round((m / n) * log(2))

  上面这几个变量关系如下所示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这个图片可以看出,当hash函数到达31个时,假阳率会最低。

确定n和p

  在实际使用布隆过滤器时,首先需要确定 n 和 p,通过上面的运算得出 m 和 k;通常可以在下面这个网站上选出合适的值

https://hur.st/bloomfilter/

  现在假设n = 4000,p = 0.000000001。我们可以自己带入公式计算m和k,也可以带入网站计算得出变量值。

n = 4000
p = 0.000000001 (1 in 1000039473)
m = 172532 (21.06KiB)
k = 30

在这里插入图片描述

选择k个hash函数

  我们发现上面计算出需要30个hash函数,难道我们要去找30个不同的hash函数来吗,显然不该这样。我们应该选择一个 hash 函数,通过给 hash 传递不同的种子偏移值,采用线性探寻的方式构造多个 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));
// k 是hash函数的个数
for (i = 0; i < k; i++) {
   Pos[i] = (hash1 + i*hash2) % m; // m 是位图的⼤⼩
}
//通过这种⽅式来模拟 k 个hash函数 跟我们前⾯开放寻址法 双重hash是⼀样的思路

题外话,面试百度:hash 函数实现过程当中 为什么 会出现 i * 31?

  • i * 31 = i * (32-1) = i * (1<<5 -1) = i << 5 - i;
  • 31 质数,hash 随机分布性很好

应用场景

  常见的处理场景:① 缓存穿透的解决;② 热 key 限流;
在这里插入图片描述

总结

在这里插入图片描述

面试题

只用2GB内存在20亿个整数中找到次数最多的数
在这里插入图片描述

分布式一致性hash

背景

  • 假设服务器只有一个缓存结点,当存储的数据越来越多时,效率就会变得很低,这时就需要增加结点分流分压
  • 使用hash来控制节点分布:hash(key) % n,在扩容时出现算法改变:hash(key) % (n+1),会造成大面积的缓存失效(即扩容后,通过新的hash算法得到的结点的存储位置和实际结点的存储位置不一致)问题

原理

在这里插入图片描述
 1. 映射空间可抽象为一个环,长度为 232,范围为[0, 232-1],每个服务器结点根据hash(node) % 232被映射到这个环上,其中node用ip:port方式来表示,比如用上图中的"192.168.1.100:6000"

 2. 判断一条数据属于哪个服务器节点的方法:根据数据哈希值,去哈希环找到第一个机器哈希值大于等于数据哈希值的机器(假设约定按顺时针查找)。如果数据的哈希值大于当前最大的机器哈希值,那么就把这个数据放在位置最靠前(哈希值最小)的机器上

 3. 由于实际机器结点往往较少,通过hash算法又具有随机性,容易导致哈希偏移问题(例如目前一共有3台机器,机器A、B的哈希值分别为1和2,而另一个机器C的哈希值为 2^32-1,那么大部分的数据都会被分给机器C)。

 4. 因此引入了虚拟节点概念,虚拟节点相当于真实节点的分身,一个真实节点可以有很多个虚拟节点,当数据被分配给这些虚拟节点时,本质上是分给这个真实节点的。数量变多了,机器结点分布的随机性会有所提高,解决了数据结点存储分布不均的问题

 5. 新增节点时:例如原本的节点哈希值列表为[1,500,1000,5000],新增节点3000后,在1001~3000范围内的数据原本是分给哈希值为5000的机器节点的,现在要把这部分数据迁移到节点5000,称为哈希迁移,参考分布式一致性hash增加或者删除节点如何进行数据迁移

 6. 删除节点:例如原本的节点哈希值列表为[1,500,1000,5000],删除节点1000后,原本范围是501~1000的数据要迁移到节点5000

说明:

  1. 哈希偏移问题产生原因:
      hash具有强随机分布性,说明它是一种概率型的算法,而概率的稳定性是跟样本数有关系的,样本数越多,概率才能稳定,因为实际机器节点往往较少(即样本数较少,会导致概率不稳定),所以很容易导致多台机器的hash值分布不均匀,甚至很可能造成有两个或多个机器的hash值非常接近。
  2. 如何解决哈希偏移问题?
      可以通过增加虚拟结点的方式,比如原本实际节点为
[] = {
"192.168.1.100:6000",
"192.168.1.101:6001",
"192.168.1.102:6002",
"192.168.1.103:6003",
}

   可以通过在端口号后面增加编号的方式:比如改成如下的方式(让每个实际节点虚拟出250个节点),然后对各个节点(包括实际节点和虚拟节点)进行hash%2^32操作,对于这种字符串比较接近的可以考虑siphash的方式对key进行hash的计算,从而让其均匀的分布在圆环上

[] = {
"192.168.1.100:6000:1",
....
"192.168.1.100:6000:250",


"192.168.1.101:6001:1",
....
"192.168.1.100:6001:250",


"192.168.1.102:6002:1",
...
"192.168.1.102:6002:250",


"192.168.1.103:6003:1",
...
"192.168.1.103:6003:250",
}

在这里插入图片描述

  1. 增加虚拟节点解决了什么问题?
  • 解决hash偏移问题(节点增加会提高hash强随机分布中概率的稳定性(因为概率的稳定性是和样本数量息息相关的))
  • 减少了hash迁移的数量(因为圆环上节点之间分布的很密集)当增加虚拟节点之后,如果后面出现扩容的情况,那么需要迁移的数据也会变少(数据迁移过程中,整个系统是不能提供服务的,直到数据迁移结束才行,所以迁移数量的减少会让系统不能提供服务的时间变短,系统的可用性就会变强
  1. 怎么根据数据的key判断最终存储在哪个分布式存储服务器上?

  首先需要有一个map结构(因为map是一个有序的结构)去存储虚拟节点的hash值以及虚拟节点的string(比如"192.168.1.103:6003:1"),所以这个map就应该是map<int, string>这种类型,类似map<hash(“192.168.1.103:6003:1”) % 2^32, “192.168.1.103:6003:1”>这种结构,然后根据数据的key去找到刚好大于这个key的虚拟节点的迭代器(通过调用map.upper_bound(key)),然后根据迭代器的->second取出类似"192.168.1.103:6003:1"的虚拟节点,最后对"192.168.1.103:6003:1"去sub操作取出服务器的ip地址和端口号即可。

  1. hash相关的固定套路
     
      以后碰到大文件,马上要想到用hash拆成小文件
     
      碰到单台机器处理不过来,马上要想到用hash分流到多台机器

  2. 为什么要使用hash?
     
      对于大文件拆分成小文件这种来说,除了把相同的数据放到同一个位置,还有要想到利用hash的强随机分布性

应用场景

  1. 分布式缓存;将数据均衡地分散在不同的服务器当中,用来分摊缓存服务器的压力;
  2. 解决缓存服务器数量变化尽量不影响缓存失效;

总结

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值