一致性Hash

569 阅读7分钟

Hash算法1

Hash算法常常使用在检索和加密中,检索如常见的HashMap、HashSet,加密如MD5和SHA算法(当前MD5、SHA-1算法已经不再符合要求2)。

此处主要是探讨数据库中Hash算法的应用,对安全性有要求,故而需要注重其在加密中的应用。此时的Hash算法必须具备两条性质:

  1. 不可逆:就是当你知道x的Hash值,无法求出x
  2. 无冲突:就是当你知道x,无法求出一个y, 使x与y的Hash值相同
  1. 实际来说,还应当满足第三条性质,确定性:相同消息的哈希结果必然相同。但是这条性质往往没那么重要或者因为容易满足而被忽略,所以平时提起Hash算法,主要关注上述两条性质
  2. Hash算法在检索和加密两种使用场景上并不是相互独立的,只是从加密的安全性考虑,其对Hash算法的不可逆性有着严格要求;而从检索的性能考虑,其对Hash算法的处理速度有着更高的要求

然而这两条性质在数学上难以成立。因为:

  1. 绝大多数函数都是可逆的
  2. Hash函数的值域有限,理论上会有无穷多个不同的原始值,它们的Hash值都相同

因此,针对这两个问题,Hash算法所能做到的是:

  1. 让求逆和求冲突在计算上变得不可能——也就是正向计算很容易,而反向计算即使穷尽人类所有的计算资源都难以完成
  2. 当冲突确实出现时,选择好的冲突解决方法,既能解决冲突,又不影响检索效率

第1条不是所有Hash算法都需要关注,仅对于安全性有要求的会考虑,且当前常用的SHA-256等Hash算法已经满足要求,故不再讨论。而第2条则是所有Hash算法都必须解决的问题,相应的就有了冲突解决策略(collision resolution policy)。

冲突解决策略

解决Hash冲突的过程,就是为插入关键字查找可用的表项单元的过程。按照发生Hash冲突时,查找的结果是将关键字放在Hash表以外的空间,还是放在Hash表以内的另一个表项单元,可以将Hash冲突解决方法分为开放式(放在表外)和闭合式(放在表内)两种。

链表法

开放散列法(open hashing)也叫链表法,其Hash表中的每个单元定义为一个链表的表头,单元中除了存放关键字外,还会保存指向链表下一单元的指针。

例如下图所示,有7个关键字的一个序列,这7个数存放在有10个单元的Hash表中,使用的Hash函数是H(K)= K mod 10。关键字的插入顺序是9877、2007、1000、9530、3013、9879和1057。结果有两个发生冲突的关键字1000和9530映射到0号单元,另有三个发生冲突的关键字9877、2007和1057映射到7号单元。

链表法

闭合散列法

英文全称closed hashing。闭合散列法把发生冲突的关键字仍然存储在当前的Hash表中,对于每个关键字K,都有一个根据Hash函数计算得到的基位置(home position) Addr(K)。如果要向Hash表中插入一个关键字K1,而之前已经有另一个关键字K2占据了K1的基位置,即Addr(K1)= Addr(K2),这时有两种方法解决K1的插入问题:

  1. Hash桶方法
  2. 线性散列法

因为Hash桶方法的常用性,这里主要对其进行讨论,而线性散列法则暂不理会。

Hash桶方法

Hash桶方法的基本概念是,把Hash表按照一定长度切分成多个桶(bucket),每个桶中的表项单元连续存放。

Hash函数首先把一个关键字映射到某一个桶的第一个单元,如果第一个单元已被占用,那么就把插入地址下移一个单元,直到找到一个可用的单元为止。如果当前桶中的全部单元都已经被填满,理论上可以把这个关键字插入到Hash表后面的一个具有无限容量的溢出桶(overflow bucket)。表中所有的桶共享同一个溢出桶。好的Hash函数可以将关键字平均地在各个Hash桶内分布,进入溢出桶的关键字尽可能少。

例如下图所示,仍然是前述的关键字序列,这次使用Hash桶结构存放。10个单元分为5个Hash桶,每个Hash桶包含2个单元。结果两个发生冲突的关键字1000和9530映射到0号Hash桶,三个发生冲突的关键字9877、2007和1057除填满了2号Hash桶之外,还占据了溢出桶的一个位置。

Hash Bucket方法

一致性Hash

一致性Hash算法是由麻省理工学院的Karger等人在解决分布式Cache中提出的,设计目标是为了解决因特网中的热点(Hot spot)问题,其实现与CARP协议(Cache Array Routing Protocol,也译作:Cache 群组路由协议)相似3

因而,在分布式中,或者更具体点在数据库中,一致性Hash算法也是为了解决负载均衡的问题。这里通过一个问题示例来引出相关内容:

假设有1000w个数据项,100个存储节点,请设计一种算法将这些数据均匀地分布在这些节点上。 更进一步:当节点数发生变化后(即扩缩容),如何继续保证数据分布的均匀性

NB:扩缩容在云时代算是一个很常见的问题了,尤其是云数据库,基本采用了计算存储分离的架构,更为注重弹性扩缩容的能力,而且快速扩缩容也算是一个比较有商业竞争力的点。因为传统数据库的扩缩容涉及到分库分表,工作量往往以天计算,对规模大一些的企业,数据库的扩缩容是很头疼的一件事

可以采用两种方法,一个是常规的Hash算法,一个是一致性Hash算法。其中,常规Hash算法就不多写了,这里推荐一篇资料,它已经写的很好了,而且有给出相应的程序示例:一致性哈希算法的理解与实践,下面着重说下一致性Hash算法

一致性Hash算法的步骤如下:

  1. 对节点进行Hash,并将其分配到0-2^{32}的圆上
  2. 对数据进行Hash,并将其映射到同样的圆上
  3. 从数据映射到的位置开始,按照顺时针方向查找,将数据保存到找到的第一个节点上。如果超过2^{32}仍然找不到节点,就将其保存到第一个节点上

如果节点数量发生了变化,比如增加了一个节点,只有在圆上增加节点的位置顺时针方向的第一台节点上的数据分布会发生变化,如下图所示:

另外,在节点数较少时,一致性Hash会因为节点在圆上的分布不均匀而引发数据倾斜。解决数据倾斜的一种方法是引入虚节点,再让虚节点与实节点映射从而尽量做到数据平衡;另一种方式引入线性映射,将Hash圆的分段映射成均匀的线性空间,再将数据直接映射到线性空间上。这两种方法都在一致性哈希算法的理解与实践中有介绍

参考资料

  1. 什么是哈希算法?
  2. 如何评价2月23日谷歌宣布实现了 SHA-1 碰撞?
  3. 一致性哈希算法原理