本文转载自微信公众号《全栈修仙之路》,作者阿宝哥。转载本文请联系全栈修真之路公众号。要理解一致性哈希,首先我们必须理解传统哈希及其在大规模分布式系统中的局限性。简而言之,散列是一种键值存储,在其中,给定一个键,可以非常有效地找到关联的值。假设我们要根据邮政编码查找城市中的街道名称。最简单的实现之一是将此信息存储为散列字典。当数据太大而无法在一个节点或机器上存储,而系统中需要多个这样的节点或机器来存储时,这个问题就变得更有趣了。例如,使用多个网络缓存中间件的系统。那么如何确定哪个key存放在哪个节点上呢?这个问题最简单的解决方法就是使用哈希取模来判断。给定一个密钥,对密钥进行哈希处理,将其除以系统中的节点数,然后将密钥放入该节点。同样,当得到一个键时,将键散列,除以节点数,然后转到该节点并获得值。上述过程对应的hash算法定义如下:node_number=hash(key)%N#其中N为节点数。下图描述了多节点系统中传统的哈希取模算法,基于该算法可以实现简单的负载均衡。1.传统哈希取模算法的局限下面分析一下传统哈希及其在大规模分布式系统中的局限性。这里直接使用我之前写的文章中你值得拥有的Bloomfilter中定义的SimpleHash类,然后对semlinker、kakuqo和test这三个key进行hash运算并取余。具体代码如下:publicclassSimpleHash{privateintcap;privateintseed;publicSimpleHash(intcap,intseed){this.cap=cap;this.seed=seed;}publicinthash(Stringvalue){intresult=0;intlen=value.length();for(inti=0;i"??+simpleHash.hash("semlinker")%3);System.out.println("node_number=hash(\"kakuqo\")%3->"??+simpleHash.hash("kakuqo")%3);System.out.println("node_number=hash(\"test\")%3->"??+simpleHash.hash("test")%3);}}上述代码运行成功后,控制台会输出如下结果:node_number=hash("semlinker")%3->1node_number=hash("kakuqo")%3->2node_number=hash("test")%3->0基于以上为了输出结果,我们可以创建下表:1.1节点缩减场景在分布式多节点系统中,故障是常见的,任何节点都可能在没有任何事先通知的情况下挂起。对于这种情况,我们希望系统只是出现性能降低,正常功能不受影响。对于原始示例,当节点发生故障时会发生什么?原例子中有3个节点,假设其中一个节点发生故障,节点数发生变化,节点数从3个减少到2个,此时表的状态发生了变化:很明显,节点的减少会导致键和节点之间的映射关系发生变化。此更改不会对新密钥产生任何影响,但会导致现有密钥的节点映射错误,以“semlinker”为例,系统变化前有3个节点,这个key对应的节点号为1,当出现故障时,节点数减为2,这个key对应的节点号key为0。1.2节点增加场景在分布式多节点系统中,一些场景如节日促销等,需要扩展服务节点以应对突发流量。对于原始示例,添加节点时会发生什么?原例子中有3个节点,假设临时增加一个节点扩容。这个时候节点的个数发生了变化,节点的个数从3个增加到4个,这时候表中节点的状态发生了变化:很明显,节点的增加也会引起之间的映射关系要更改的密钥和节点。这个改动不会对新的key有任何影响,但是对于已有的key,会导致nodeMapping错误,同样以“semlinker”为例,改动前系统有3个node,这个对应的node号key为1,增加节点时,节点数增加到4,这个key对应的节点号为2。当集群中的节点数量发生变化时,之前的映射规则可能会发生变化。如果集群中每台机器提供的服务无法区分,这将没有任何效果。但是对于分布式缓存这样的系统来说,映射规则的失效就意味着之前缓存的失效。如果同时出现大量缓存失效,可能会出现“缓存雪崩”,造成灾难性的后果。为了解决这个问题,我们必须重新分配剩余节点上的所有现有密钥,这可能是一项非常昂贵的操作,并且会对正在运行的系统产生不利影响。当然,除了重新分配所有现有密钥的方案之外,还有一种更好的方案,即使用一致性哈希算法。2.一致性哈希算法一致性哈希算法于1997年由麻省理工学院提出,它是一种特殊的哈希算法,可以在移除或添加服务器时尽可能少地改变现有服务。请求与处理请求服务器的映射关系。一致性哈希解决了分布式哈希表(DistributedHashTable,DHT)中简单哈希算法的动态伸缩问题。2.1一致性哈希算法可扩展性的优势。一致性哈希算法确保在增加或减少服务器时,数据存储的变化最小,与传统哈希算法相比,大大节省了数据移动的开销。更好地适应数据的快速增长。一致性哈希算法用于分发数据。当数据不断增长时,一些虚拟节点可能包含大量数据,导致虚拟节点上的数据分布不均衡。此时,可以拆分包含更多数据的虚拟节点。这种分裂只是将原来的虚拟节点一分为二,不需要对所有数据进行重新哈希划分。虚拟节点拆分后,如果物理服务器负载仍然不均衡,只需要调整部分虚拟节点在服务器间的存储分布即可。这样,物理服务器的数量可以随着数据的增长而动态扩展,成本远低于传统的哈希算法重新分配所有数据的成本。2.2一致性哈希算法与哈希算法的关系一致性哈希算法是在哈希算法的基础上提出来的。在一个动态变化的分布式环境中,哈希算法应该满足几个条件:平衡性、单调性和分散性。Balance:就是哈希结果要平均分配到各个节点,从算法上解决了负载均衡问题。单调性:是指在增加或删除节点时,不会影响系统的正常运行。去中心化:是指数据应该存储在分布式集群中的各个节点中(节点本身可以??有备份),不需要每个节点都存储所有的数据。3.一致性哈希算法原理一致性哈希算法是通过一种称为一致性哈希环的数据结构来实现的。这个环的起点是0,终点是2^32-1,起点和终点相连,所以这个环的整数分布范围是[0,2^32-1],如如下图所示:3.1将对象放入哈西环假设我们有四个对象“semlinker”、“kakuqo”、“lolo”和“fer”,分别缩写为o1、o2、o3和o4,然后使用hash函数计算这个对象的hash值,取值范围是[0,2^32-1]:图中对象的映射关系如下:hash(o1)=k1;hash(o2)=k2;hash(o3)=k3;hash(o4)=k4;3.2发送服务器放到哈希环中,然后使用相同的哈希函数。我们还将服务器放在哈希环上。可以选择服务器的IP或者主机名作为哈希的key,这样每台服务器都可以确定自己在哈希环上的位置这里假设我们有3台缓存服务器,分别是cs1、cs2和cs3:图中服务器的映射关系如下:hash(cs1)=t1;hash(cs2)=t2;hash(cs3)=t3;#CacheServer3.3对象选择服务器和服务器在同一个hashring上,在hashring上顺时针查找距离对象的hash值最近的机器,就是该对象所属的机器。以o2对象为例,sequenceneedle找到最近的机器是cs2,所以服务器cs2会缓存o2对象。而服务器cs1缓存o1、o3对象,服务器cs3缓存o4对象。3.4服务器增加情况假设由于业务需要,我们需要增加一台服务器cs4。经过同样的哈希运算,服务器最终会落在t1和t2服务器之间,如下图所示:对于上述情况,只需要重新分配t1和t2服务器之间的对象。在上面的例子中,只有o3对象需要重新分配,即它被重新定位到cs4服务器。之前我们分析过,如果使用简单的取模方式,在添加新服务器的时候,大部分缓存可能会失效,但是使用一致性哈希算法后,这种情况得到了很大的改善,因为只有少数几个对象需要重新分配.3.5服务器缩减情况假设cs3服务器出现故障,服务下线。这时需要将原本存放在cs3服务器上的对象o4重新分配到cs2服务器上,其他的对象还是存放在原来的机器上。3.6虚拟节点一致性哈希的基本原理已经介绍到这里,但是在新服务器的情况下还存在一些问题。新增加的服务器cs4只分担了cs1服务器的负载,服务器cs2和cs3的负载压力并没有因为cs4服务器的增加而减少。如果cs4服务器的性能与旧服务器一样好,甚至可能更好,这不是我们所期望的。针对这个问题,我们可以通过引入虚拟节点来解决负载不均衡的问题。即把每台物理服务器虚拟成一组虚拟服务器,虚拟服务器放在哈希环上。如果要确定对象的服务器,则需要先确定对象的虚拟服务器,再由虚拟服务器确定物理服务器。图中o1和o2代表对象,v1~v6代表虚拟服务器,s1~s3代表物理服务器。4.一致性哈希算法的实现这里只介绍不带虚拟节点的一致性哈希算法的实现:importjava.util.SortedMap;importjava.util.TreeMap;publicclassConsistentHashingWithoutVirtualNode{//加入哈希环的服务器列表privatestaticString[]servers={"192.168.0.1:8888","192.168.0.2:8888","192.168.0.3:8888"};//key表示服务器的hash值,value表示服务器privatestaticSortedMapsortedMap=newTreeMap();//程序初始化,将所有服务器放入sortedMapstatic{for(inti=0;isubMap=sortedMap.tailMap(hash);if(subMap.isEmpty()){//如果没有大于key的hash值,从第一个节点开始Integeri=sortedMap.firstKey();//返回对应的服务器returnsortedMap.get(i);}else{//第一个Key是顺时针过去距离节点最近的节点Integeri=subMap.firstKey();//返回对应的服务器returnssubMap.get(i);}}//使用FNV1_32_HASH算法计算服务器的Hash值privatestaticintgetHash(Stringstr){finalintp=16777619;inthash=(int)2166136261L;for(inti=0;i>7;hash+=hash<<3;hash^=hash>>17;hash+=hash<<5;//如果计算的值为负数,取其绝对值if(hash<0)hash=Math.abs(hash);returnhash;}publicstaticvoidmain(String[]args){String[]keys={"semlinker","kakuqo","fer"};for(inti=0;i