Hash表查找与分布式一致性Hash

1、背景

如何从海量数据中查询某个字符串是否存在?如果使用有序数据采用从前向后的对比查找,时间复杂度为O(n),如果使用折半查找(平衡二叉树),时间复杂度为O( l o g 2 n log_2{n} log2n)。对于海量数据查找,使用Hash表查询会更高效。

2、Hash表概述

Hash表又称散列表,基本思路是:设要存储的元素个数是n,设置一个长度为m(m≥n)的连续内存单元(数组)。找到一个哈希函数并计算要存储的值,得到一个哈希值,使得这个哈希值与内存单元地址存在映射关系(也可以与数据下标存在映射关系)。通过哈希函数计算每个要存储的元素,将得到的哈希值存储到对应的映射地址中。把如此构造的线性存储结构称为哈希表。

哈希冲突:不同的存储元素通过同一哈希函数计算出相同的哈希值,导致多个元素存储在同一个地址中,这就是哈希冲突。若哈希函数选择得当,就可以使得计算出的哈希值尽可能均匀的分布在哈希表中,从而尽可能的减少冲突。

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

3、常用的哈希函数构造方法

1)直接地址法

直接地址法是以关键字k或关键字加上某个常量c作为哈希地址的方法,例: h(k)=k+c

这种方法的特点就是哈希函数计算简单。当关键字基本连续时,可使用这种方式。如果关键字随机性很强,使用这种方式比较浪费内存空间(很多地址空间都没用到)。

2)取余法

取余法是用关键字k除以某个不大于哈希表长度m的一个整数p所得的余数作为哈希地址,例:h(k)=k%p (p≤m)

取余法的计算比较简单,适用范围比较广,是最经常使用的一种哈希函数。这种方法的关键是选好p,使得每个关键字通过该函数转换后映射到哈希表的范围内任意地址上的概率是相等的,从而尽可能减少发生冲突的可能性。理论研究表明:p取奇数比取偶数的效果好,且p取不大于m的素数效果最好。

3)常用哈希函数

murmurhash1,murmurhash2,murmurhash3,siphash(redis6.0当中使⽤,rust等大多数语言选用的hash算法来实现hashmap),cityhash 都具备强随机分布性;

  • siphash 主要解决字符串接近时的强随机分布性 ; 常用于Redis,因为Redis是key-value存储,而且key经常是字符串且非常接近,例如:用户100001、用户100002等

4、处理冲突方式

1)线性探测法

线性探测法的数学描述:

d 0 = h ( k ) d_0=h(k) d0=h(k)

d i = ( d i − 1 + 1 ) d_i=(d_{i-1}+1) di=(di1+1)%m (1≤i≤m-1)

实现过程:当计算的 d 0 d_0 d0发生冲突时,说明需要找其他位置存储,这时就使用 d i = ( d i − 1 + 1 ) d_i=(d_{i-1}+1) di=(di1+1)%m公式计算哈希值,然后找空位置,如果还存在冲突继续使用第二个公式计算并查找空位置。

使用线性探测法解决冲突优点是计算简单,一个重大缺点是容易产生堆积问题。当连续出现若干同义词时:设第一个同义词占用 d 0 d_0 d0位置,其他连续同义词占用 d 1 d_1 d1 d 2 d_2 d2 d 3 d_3 d3,当又来了一个关键字计算的哈希值落在 d 2 d_2 d2,但是他的位置被占了,还需要通过第二个公式找空位置。这会使得原本没有冲突的关键字却发生了冲突。冲突的越多,找位置的次数也会越多。

2)平方探测法

平方探测法的探测序列为: d 0 + 1 2 d_0+1^2 d0+12 d 0 − 1 2 d_0-1^2 d012 d 0 + 2 2 d_0+2^2 d0+22 d 0 − 2 2 d_0-2^2 d022…(交替加减)。平方探测法的数学描述:

d 0 = h ( k ) d_0=h(k) d0=h(k)

d i = ( d 0 ± i 2 ) d_i=(d_0±i^2) di=(d0±i2)%m (1≤i≤m-1)

通过公式可以看出,平方探测法就是在它被占用的位置前后来回找空位置。这样做的好处就是避免了堆积的问题,其缺点是不一定能够探测到哈希表的所有位置,但最少能探测到一半的位置。

以上两种方法都属于开放地址法:就是在出现哈希冲突的时候再哈希表中找到一个新的空闲位置存放元素。此外,除了上述的两种开放地址法,还有随机序列法(探测位置是随机的)、双哈希函数法(设计某种哈希函数,里面包含两次哈希计算)。

3)拉链法

使用这种方法时,哈希表中存放的是指针(而不是关键字本身),当计算的关键字哈希值符合某个地址(或者数组下标),就会让该位置的指针指向存储的关键字。如果出现出现哈希冲突,就在该位置继续使用链表将所有冲突的关键字连接起来。

当hash值相同一般采用头插法在节点的单链表中插入元素:因为数据库中一般认为最先插入的数据,也会最先被使用到

与开放地址法相比,拉链法的优点:

  1. 非同义词绝不会发生冲突,不会发生堆积情况,平均查找速度比较短
  2. 由于拉链法中各单链表上的节点是动态申请的,故它更适合造表前无法确定表长的情况
  3. 删除节点操作更加容易实现

缺点:指针存储需要额外的空间,当数据过多时,占用的空间比开放地址法大得多

拉链法的应用:
Java中的HashMap使用的就是拉链法存储。当一个节点的单链表过于长时,会影响查找效率,此时就需要将单链表转换为红黑树,一般来说当单链表长度达到256(经验值),会进行转换。

5、分布式一致性 hash

应用:分布式一致性 hash主要用于解决分布式缓存,将数据均衡地分散在不同的服务器当中,用来分摊缓存服务器的压力,解决缓存服务器数量变化尽量不影响缓存失效

1)算法实现

  • 分布式一致性 hash 算法将哈希空间组织成一个虚拟的圆环,数据结构上就是一个数组,通过取余就可实现圆环效果

  • 圆环的大小是 ;算法为: hash(ip)% 2 32 2^{32} 232 ,最终会得到一个 [0, 2 32 2^{32} 232-1] 之间的一个无符号整型,这个整数代表服务器的编号

  • 多个服务器都通过这种方式在 hash 环上映射一个点来标识该服务器的位置;当用户操作某个 key,通过同样的算法生成一个值,根据此值沿环顺时针定位某个服务器(顺时针遇到的第一个服务器),那么该 key 就在该服务器中,然后通过key取出value值

【Tips:将对2的次幂取余,转换为位运算的方式:m % 2 n 2^n 2n = m & ( 2 n 2^n 2n-1),位运算比加减乘除计算快】
在这里插入图片描述

2)哈希偏移问题

  • 两个服务器之间的区域,属于顺时针那个服务器的工作载荷,服务器的分布越均匀,缓存载荷就分布的越均匀

  • hash 算法得到的结果是随机的,不能保证服务器节点均匀分布在哈希环上(可能两个服务器之间离得很近),分布不均匀造会成请求访问不均匀,服务器承受的压力也就不均匀

  • 如下图所示,大部分的客户端请求压力集中在服务器1和服务器0上,这样显然是不合理的
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G6M52xkJ-1639121980030)(C:\Users\XueP.H\AppData\Roaming\Typora\typora-user-images\image-20211210153038070.png)]
    3)解决办法:插入虚拟节点

  • 为了解决哈希偏移的问题,增加了虚拟节点的概念;理论上,哈希环上节点数越多,数据分布越均衡

  • 为每个服务节点计算多个哈希节点(虚拟节点),通常做法是:hash(“IP:PORT:seqno”) % 2 32 2^{32} 232

  • 具体操作是:先使用算法计算出多个节点,此时的分布较为均匀,在圆上选择真正的服务器节点(选择时要尽量均匀),剩余其他节点为虚拟节点。当计算的key值落到虚拟节点的区间上,就顺时针找从属的真正服务器节点,让其处理缓存的请求。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YqqOqFCD-1639121980031)(C:\Users\XueP.H\AppData\Roaming\Typora\typora-user-images\image-20211210153313542.png)]

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值