海量数据去重的hash与BloomFilter

背景

  • 使用 word 文档时, word 如何判断某个单词是否拼写正确?
  • 垃圾邮件过滤算法如何设计?
  • 网络爬虫程序,怎么让它不去爬相同的 url 页面?

以上都是基于某些字符串为目标,通过hash在海量的数据中查询筛选出的应用背景

常用的高效查找--二分查找

原理:前提先是保证数据的有序性,再通过比较每次排除一半的元素达到快速索引的目的

采用二分查找的数据结构:

  • 有序的数组
  • 平衡二叉树( AVL树 和 红黑树)
  • 平衡多路搜索树(B-tree & B+tree)
  • 多层级有序链表(跳表)

时间复杂度:O( log_{2}n ),意味着当在100万个数据中查找,最多比较20次就能找到目标,10亿个数据最多比较30次便可找到目标

散列表

散列技术与二分查找法相比,最大的不同是它摒弃了“关键码有序”的先决条件,在海量的数据中若通过二分查找比较字符串来查询数据会大大降低查找的效率,而散列表则采用的是 基于key通过一个hash函数计算出key在表中的位置地址,是key和其所在存储地址的映射关系,从而大大提升搜索效率

 散列表的组成

  • hash函数

映射函数 Hash(key)=addr hash 函数可能会把两个或两个以 上的不同 key 映射到同一地址,这种情况称之为 冲突 (或者 hash 碰撞);
  • 数组

 通过hash函数计算key在数组中的地址,再将kv节点存放在这个地址中,由此组成整个数组

 hash的选择条件

  • 计算速度快
  • 强随机分布(等概率、均匀地分布在整个地址空间)

开发中的hash函数选择 

  • murmurhash1(计算速度最快,但质量一般)
  • murmurhash2(计算速度快,质量也有保证,其使用最为广泛)
  • murmurhash3(计算速度比较慢,但质量是最好的) 
  • siphash 主要解决字符串接近的强随机分布性 redis6.0 当中使用,rust 等大多数语言选用的 hash 算法来实hashmap
  • cityhash(同样具有强随机分布性,应用也比较广泛)

作用:更快更好地实现key与存储地址的映射关系

 散列表的操作流程

  • 插入
  • 搜索

 

 无论插入还是搜索,都是一个key值通过hash函数的多次运算得到相同的值,这个值对不变的数组长度进行取余操作,得到数组中对应的槽位地址,从而定位到节点的存储位置,实现搜索和插入

hash冲突及其解决

  • 冲突

若两个或以上的key通过hash函数的计算,可能被映射到数组的同一个地址,从而造成hash冲突;

  •  负载因子 (描述冲突的激烈程度)
数组存储元素的个数 / 数组长度,也是用来形容散列表的存储密度; 负载因子越小,冲突概率越小,负载因子越大,冲突概率越大;

冲突的解决

前提:负载因子在合理范围内,即 数组存储元素的个数  < 数组的长度 

  • 链表法(拉链法):将冲突的元素通过链表的连接方式存放在同一数组槽位地址中,这也是冲用的处理冲突的方法,但当冲突元素过多使链表的长度过大,从而造成极端情况,这个时候可以将这个链表转换为红黑树、最小堆,由原来链表时间复杂度转换为红黑树时间复杂度;那判断该链表过长的依据是多少?可以采用超过 256(经验值)个节点的时候将链表结构转换为红黑树或堆结构;(拉链法的数组是指针数组,每个槽位都是指针)
  • 开放寻址法:同构算法将所有的元素都存放在哈希表的数组中,不使用额外的数据结构;一般使用线性探查的思路解决;

         步骤:

                1、当插入新元素的时,使用哈希函数在哈希表中定位元素位置;

                2、检查数组中该槽位索引是否存在元素。如果该槽位为空,则插入,否则3;

                3、在 2 检测的槽位索引上加一定步长接着检查2; 加一定步长分为以下几种:

                        · i+1,i+2,i+3,i+4, ... ,i+n

                        · i-1² ,i+2² ,i-3² ,1+4² , ... 

                        这两种都会导致同类 hash 聚 集;也就是近似值它的hash值也近似,那么它的数组槽位也靠近,形成 hash 聚集;第一种同类聚集冲突在前,第二种只是将聚集冲突延后;

                        另外还可以使用双重哈希 来解决上面出现hash聚集现象:

                        

当负载因子不在合理范围内时,即 实际的存储元素 < 数组的 0.1size  或  实际的存储元素 > 数组实际的存储元素 ,要想解决冲突要对数组进行 缩容 或 扩容 ,缩容只是为了避免存储空间的浪费,而扩容才是真正的解决中途,而后还要进行rehash(重新哈希) ,因为扩容是翻倍扩容,算法公式:hash(key) % size = Index 中的 size 已经发生改变,所以要重新计算

STL unordered_* 散列表实现

 在 STL 中 unordered_map、unordered_set、 unordered_multimap、unordered_multiset 四兄弟底层实 现都是散列表;

原理图:采用拉链法解决冲突,优化:为了实现迭代器,将数组中的各个链表或节点串成一个单链表

  布隆过滤器       

 背景

当要确认一个key是否在文件中,可以将整个文件加载到内存中,然后再进行查询判断,但由于内存有限,一般不选择这种方式,且我们只需要知道key是否存在,而不用获取对组中的value内容,所以选择先将每个对组元素中的key标记到布隆过滤器中,然后再将每个 keyvalue对 存入文件中,最后便可直接通过布隆过滤器判断出key是否在文件中了

 构成

位图(bitmap 每一槽位只存放0或1的数组)+  n个hash函数

通过hash函数和一些公式计算元素的存储位置,举例:

 确定索引位置的操作:hash(key) %bit_size = Index

原理

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

为什么不支持删除操作

在位图中每个槽位只有两种状态(0 或者 1),一个槽位被 设置为 1 状态,但不确定它被设置了多少次;也就是不知道被多少个 key 哈希映射而来以及是被具体哪个 hash 函数映 射而来;

 如上图:在所圈的槽位中因被映射而置为了1,但却无法确定它是由哪个hash函数映射而来,也无法确认有多少个str的key通过hash映射到该槽位,若因为不需要某个key而将该槽位的状态改为0,则会影响对其他key是否存在的判断

应用场景

布隆过滤器通常用于判断某个 key 一定不存在的场景,同时允
许判断存在时有误差的情况;
常见处理场景:① 缓存穿透的解决;② 热 key 限流;

布隆过滤器的参数和计算公式

根据 n 和 p,算出 m 和 k

总结

  • 布隆过滤器是一种概率型数据结构,它的特点是高效地插入和 查询,能确定某个字符串一定不存在或者可能存在
  • 布隆过滤器不存储具体数据,所以占用空间小,查询结果存在误差,但是误差可控,同时不支持删除操作

分布式一致性hash

 背景

随着存储数据的增加,我们需要将数据存储到不同的节点的kv中,随之就需要对缓存进行扩容,就是增加存储的节点,但存储 kv对 的地址会被公式 hash(key) % size 映射出对应的索引值Index并存储在缓存当中,若要扩容就会改变size的值 从而改变原来 kv对 存储地址映射在缓存的索引值,造成无法通过缓存中的索引值找到 kv对 真正的存储地址,即造成缓存失效

解决:可以固定算法,即将算法中的变量size固定下来,将size固定为 2^32 ,但我们真的要设置一个 2^32 大小的数组来进行存储吗?显然不现实,所以还要改变查找节点映射关系,即改变计算映射的算法,分布式一致性hash便采用了此方法

算法原理

分布式一致性 hash 算法将哈希空间组织成一个虚拟的圆环,圆
环的大小是;
算法为:hash(ip) %2^32,最终会得到一个 [0 , 2^32 - 1 ] 之间 的一个无符号整型,这个整数代表服务器的编号;多个服务器 都通过这种方式在 hash 环上映射一个点来标识该服务器的位 置;当用户操作某个 key,通过同样的算法生成一个值,沿环顺时针定位某个服务器,那么该 key 就在该服务器中;如下图所示 (图片来自互联网)

应用场景 

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

仍存在的问题及以解决 

问题1

如上图,当在 .102 和 .100 的服务器地址区间插入新的服务区地址 .103 时,根据顺时针规定 会造成在原本在 .102 和 .100 区间 存放在.100地址的节点 的映射索引有部分变成 .103地址的映射索引,但我们在.103地址寻找是查询不到原来的节点,像上图就是会无法在 .103地址中找到k3节点数据,因此造成部分的缓存失效。

解决方法:hash迁移

仍如上图,将在 .102 到 .103区间原本存储在 .100 地址的节点数据全部迁移到 .103 中,但仍没有完全解决局部缓存失效的问题;

 

问题2

hash偏移:hash 算法得到的结果是随机的,不能保证服务器节点均匀分布在哈希环上;分布不均匀造成请求访问不均匀,服务器承受的压力不均匀;                                                                     本质是存储的样本数太少造成的,因为样本数越大hash算法的才越稳定,节点的分布才会更均匀分布在哈希环地址中;

解决方法:引入虚拟节点

为了解决哈希偏移的问题,增加了虚拟节点的概念;理论上, 哈希环上节点数越多,数据分布越均衡;
为每个服务节点计算多个哈希节点(虚拟节点),通常做法是:
hash("IP:PORT:seqno") % 2^32 ;举例如下图:

在一个服务器端口地址后面增加 : 6000 : "1~255 中的编号",则带有 .100编号地址的节点就会随机均匀地分布在哈希环上 ,其他的端口地址同理也可引入多个虚拟节点。

推荐一个零声学院免费公开课程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:零声教育


 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值