一致性Hash代码实现

一致性Hash算法

关于一致性Hash算法,在我以前的博文中已经有屡次提到了,MemCache超详细解读一文中"一致性Hash算法"部分,对于为何要使用一致性Hash算法和一致性Hash算法的算法原理作了详细的解读。java

算法的具体原理这里再次贴上:node

先构造一个长度为2 32 的整数环(这个环被称为一致性Hash环),根据节点名称的Hash值(其分布为[0, 2 32-1])将服务器节点放置在这个Hash环上,而后根据数据的Key值计算获得其Hash值(其分布也为[0, 2 32-1]),接着在Hash环上顺时针查找距离这个Key值的Hash值最近的服务器节点,完成Key到服务器的映射查找。算法

这种算法解决了普通余数Hash算法伸缩性差的问题,能够保证在上线、下线服务器的状况下尽可能有多的请求命中原来路由到的服务器。编程

固然,万事不可能十全十美,一致性Hash算法比普通Hash算法更具备伸缩性,可是同时其算法实现也更为复杂,本文就来研究一下,如何利用Java代码实现一致性Hash算法。在开始以前,先对一致性Hash算法中的几个核心问题进行一些探究。数组

数据结构的选取

一致性Hash算法最早要考虑的一个问题是:构造出一个长度为2 32 的整数环,根据节点名称的Hash值将服务器节点放置在这个Hash环上。服务器

那么,整数环应该使用何种数据结构,才能使得运行时的时间复杂度最低?首先说明一点,关于时间复杂度, 常见的时间复杂度与时间效率的关系有以下的经验规则:数据结构

O(1) < O(log 2 N) < O(n) < O(N * log 2 N) < O(N 2 ) < O(N 3 ) < 2N < 3N < N!负载均衡

通常来讲,前四个效率比较高,中间两个差强人意,后三个比较差(只要N比较大,这个算法就动不了了)。OK,继续前面的话题,应该如何选取数据结构,我认为有如下几种可行的解决方案。分布式

一、解决方案一:排序+List

我想到的第一种思路是:算出全部待加入数据结构的节点名称的Hash值放入一个数组中,而后使用某种排序算法将其从小到大进行排序,最后将排序后的数据放入List中,采用List而不是数组是为告终点的扩展考虑。性能

以后,待路由的结点,只须要在List中找到第一个Hash值比它大的服务器节点就能够了 ,好比服务器节点的Hash值是[0,2,4,6,8,10],带路由的结点是7,只须要找到第一个比7大的整数,也就是8,就是咱们最终须要路由过去的服务器节点。

若是暂时不考虑前面的排序,那么这种解决方案的时间复杂度:

(1)最好的状况是第一次就找到,时间复杂度为O(1)

(2)最坏的状况是最后一次才找到,时间复杂度为O(N)

平均下来时间复杂度为O(0.5N+0.5),忽略首项系数和常数,时间复杂度为O(N)。

可是若是考虑到以前的排序,我在网上找了张图,提供了各类排序算法的时间复杂度:

看得出来,排序算法要么稳定可是时间复杂度高、要么时间复杂度低但不稳定,看起来最好的归并排序法的时间复杂度仍然有O(N * logN),稍微耗费性能了一些。

二、解决方案二:遍历+List

既然排序操做比较耗性能,那么能不能不排序?能够的,因此进一步的,有了第二种解决方案。

解决方案使用List不变,不过能够采用遍历的方式:

(1)服务器节点不排序,其Hash值所有直接放入一个List中

(2)带路由的节点,算出其Hash值,因为指明了"顺时针",所以遍历List,比待路由的节点Hash值大的算出差值并记录,比待路由节点Hash值小的忽略

(3)算出全部的差值以后,最小的那个,就是最终须要路由过去的节点

在这个算法中,看一下时间复杂度:

一、最好状况是只有一个服务器节点的Hash值大于带路由结点的Hash值,其时间复杂度是O(N)+O(1)=O(N+1),忽略常数项,即O(N)

二、最坏状况是全部服务器节点的Hash值都大于带路由结点的Hash值,其时间复杂度是O(N)+O(N)=O(2N),忽略首项系数,即O(N)

因此,总的时间复杂度就是O(N)。其实算法还能更改进一些:给一个位置变量X,若是新的差值比原差值小,X替换为新的位置,不然X不变。这样遍历就减小了一轮,不过通过改进后的算法时间复杂度仍为O(N)。

总而言之,这个解决方案和解决方案一相比,整体来看,彷佛更好了一些。

三、解决方案三:二叉查找树

抛开List这种数据结构,另外一种数据结构则是使用 二叉查找树 。对于树不是很清楚的朋友能够简单看一下这篇文章树形结构。

固然咱们不能简单地使用二叉查找树,由于可能出现不平衡的状况。平衡二叉查找树有AVL树、红黑树等,这里使用红黑树,选用红黑树的缘由有两点:

一、红黑树主要的做用是用于存储有序的数据,这其实和第一种解决方案的思路又不谋而合了,可是它的效率很是高

二、JDK里面提供了红黑树的代码实现TreeMap和TreeSet

另外,以TreeMap为例,TreeMap自己提供了一个tailMap(K fromKey)方法,支持从红黑树中查找比fromKey大的值的集合,但并不须要遍历整个数据结构。

使用红黑树,可使得查找的时间复杂度下降为O(logN),比上面两种解决方案,效率大大提高。

为了验证这个说法,我作了一次测试,从大量数据中查找第一个大于其中间值的那个数据,好比10000数据就找第一个大于5000的数据(模拟平均的状况)。看一下O(N)时间复杂度和O(logN)时间复杂度运行效率的对比:

  50000 100000 500000 1000000 4000000
ArrayList 1ms 1ms 4ms 4ms 5ms
LinkedList 4ms 7ms 11ms 13ms 17ms
TreeMap 0ms 0ms 0ms 0ms 0ms

由于再大就内存溢出了,因此只测试到4000000数据。能够看到,数据查找的效率,TreeMap是完胜的,其实再增大数据测试也是同样的,红黑树的数据结构决定了任何一个大于N的最小数据,它都只须要几回至几十次查找就能够查到。

固然,明确一点,有利必有弊,根据我另一次测试获得的结论是, 为了维护红黑树,数据插入效率TreeMap在三种数据结构里面是最差的,且插入要慢上5~10倍 。

Hash值从新计算

服务器节点咱们确定用字符串来表示,好比"192.168.1.1"、"192.168.1.2",根据字符串获得其Hash值,那么另一个重要的问题就是 Hash值要从新计算,这个问题是我在测试String的hashCode()方法的时候发现的,不妨来看一下为何要从新计算Hash值:

System.out.println("192.168.0.0:111的哈希值:" + "192.168.0.0:1111".hashCode());
 System.out.println("192.168.0.1:111的哈希值:" + "192.168.0.1:1111".hashCode());
 System.out.println("192.168.0.2:111的哈希值:" + "192.168.0.2:1111".hashCode());
 System.out.println("192.168.0.3:111的哈希值:" + "192.168.0.3:1111".hashCode());
 System.out.println("192.168.0.4:111的哈希值:" + "192.168.0.4:1111".hashCode());

咱们在作集群的时候,集群点的IP以这种连续的形式存在是很正常的。看一下运行结果为:

192.168.0.0:111的哈希值:1845870087
192.168.0.1:111的哈希值:1874499238
192.168.0.2:111的哈希值:1903128389
192.168.0.3:111的哈希值:1931757540
192.168.0.4:111的哈希值:1960386691

这个就问题大了,[0,2 32 -1]的区间之中,5个HashCode值却只分布在这么小小的一个区间,什么概念?[0,2 32 -1]中有4294967296个数字,而咱们的区间只有122516605,从几率学上讲这将致使97%待路由的服务器都被路由到"192.168.0.1"这个集群点上,简直是糟糕透了!

另外还有一个很差的地方:规定的区间是非负数,String的hashCode()方法却会产生负数(不信用"192.168.1.0:1111"试试看就知道了)。不过这个问题好解决,取绝对值就是一种解决的办法。

综上,String重写的hashCode()方法在一致性Hash算法中没有任何实用价值,得找个算法从新计算HashCode。这种从新计算Hash值的算法有不少,好比CRC32_HASH、FNV1_32_HASH、KETAMA_HASH等,其中KETAMA_HASH是默认的MemCache推荐的一致性Hash算法,用别的Hash算法也能够,好比FNV1_32_HASH算法的计算效率就会高一些。

对一致性Hash算法,Java代码实现的深刻研究

一致性Hash算法

关于一致性Hash算法,在我以前的博文中已经有屡次提到了,MemCache超详细解读一文中"一致性Hash算法"部分,对于为何要使用一致性Hash算法和一致性Hash算法的算法原理作了详细的解读。

算法的具体原理这里再次贴上:

先构造一个长度为2 32 的整数环(这个环被称为一致性Hash环),根据节点名称的Hash值(其分布为[0, 2 32-1])将服务器节点放置在这个Hash环上,而后根据数据的Key值计算获得其Hash值(其分布也为[0, 2 32-1]),接着在Hash环上顺时针查找距离这个Key值的Hash值最近的服务器节点,完成Key到服务器的映射查找。

这种算法解决了普通余数Hash算法伸缩性差的问题,能够保证在上线、下线服务器的状况下尽可能有多的请求命中原来路由到的服务器。

固然,万事不可能十全十美,一致性Hash算法比普通Hash算法更具备伸缩性,可是同时其算法实现也更为复杂,本文就来研究一下,如何利用Java代码实现一致性Hash算法。在开始以前,先对一致性Hash算法中的几个核心问题进行一些探究。

数据结构的选取

一致性Hash算法最早要考虑的一个问题是:构造出一个长度为2 32 的整数环,根据节点名称的Hash值将服务器节点放置在这个Hash环上。

那么,整数环应该使用何种数据结构,才能使得运行时的时间复杂度最低?首先说明一点,关于时间复杂度, 常见的时间复杂度与时间效率的关系有以下的经验规则:

O(1) < O(log 2 N) < O(n) < O(N * log 2 N) < O(N 2 ) < O(N 3 ) < 2N < 3N < N!

通常来讲,前四个效率比较高,中间两个差强人意,后三个比较差(只要N比较大,这个算法就动不了了)。OK,继续前面的话题,应该如何选取数据结构,我认为有如下几种可行的解决方案。

一、解决方案一:排序+List

我想到的第一种思路是:算出全部待加入数据结构的节点名称的Hash值放入一个数组中,而后使用某种排序算法将其从小到大进行排序,最后将排序后的数据放入List中,采用List而不是数组是为告终点的扩展考虑。

以后,待路由的结点,只须要在List中找到第一个Hash值比它大的服务器节点就能够了 ,好比服务器节点的Hash值是[0,2,4,6,8,10],带路由的结点是7,只须要找到第一个比7大的整数,也就是8,就是咱们最终须要路由过去的服务器节点。

若是暂时不考虑前面的排序,那么这种解决方案的时间复杂度:

(1)最好的状况是第一次就找到,时间复杂度为O(1)

(2)最坏的状况是最后一次才找到,时间复杂度为O(N)

平均下来时间复杂度为O(0.5N+0.5),忽略首项系数和常数,时间复杂度为O(N)。

可是若是考虑到以前的排序,我在网上找了张图,提供了各类排序算法的时间复杂度:

看得出来,排序算法要么稳定可是时间复杂度高、要么时间复杂度低但不稳定,看起来最好的归并排序法的时间复杂度仍然有O(N * logN),稍微耗费性能了一些。

二、解决方案二:遍历+List

既然排序操做比较耗性能,那么能不能不排序?能够的,因此进一步的,有了第二种解决方案。

解决方案使用List不变,不过能够采用遍历的方式:

(1)服务器节点不排序,其Hash值所有直接放入一个List中

(2)带路由的节点,算出其Hash值,因为指明了"顺时针",所以遍历List,比待路由的节点Hash值大的算出差值并记录,比待路由节点Hash值小的忽略

(3)算出全部的差值以后,最小的那个,就是最终须要路由过去的节点

在这个算法中,看一下时间复杂度:

一、最好状况是只有一个服务器节点的Hash值大于带路由结点的Hash值,其时间复杂度是O(N)+O(1)=O(N+1),忽略常数项,即O(N)

二、最坏状况是全部服务器节点的Hash值都大于带路由结点的Hash值,其时间复杂度是O(N)+O(N)=O(2N),忽略首项系数,即O(N)

因此,总的时间复杂度就是O(N)。其实算法还能更改进一些:给一个位置变量X,若是新的差值比原差值小,X替换为新的位置,不然X不变。这样遍历就减小了一轮,不过通过改进后的算法时间复杂度仍为O(N)。

总而言之,这个解决方案和解决方案一相比,整体来看,彷佛更好了一些。

三、解决方案三:二叉查找树

抛开List这种数据结构,另外一种数据结构则是使用 二叉查找树 。对于树不是很清楚的朋友能够简单看一下这篇文章树形结构。

固然咱们不能简单地使用二叉查找树,由于可能出现不平衡的状况。平衡二叉查找树有AVL树、红黑树等,这里使用红黑树,选用红黑树的缘由有两点:

一、红黑树主要的做用是用于存储有序的数据,这其实和第一种解决方案的思路又不谋而合了,可是它的效率很是高

二、JDK里面提供了红黑树的代码实现TreeMap和TreeSet

另外,以TreeMap为例,TreeMap自己提供了一个tailMap(K fromKey)方法,支持从红黑树中查找比fromKey大的值的集合,但并不须要遍历整个数据结构。

使用红黑树,可使得查找的时间复杂度下降为O(logN),比上面两种解决方案,效率大大提高。

为了验证这个说法,我作了一次测试,从大量数据中查找第一个大于其中间值的那个数据,好比10000数据就找第一个大于5000的数据(模拟平均的状况)。看一下O(N)时间复杂度和O(logN)时间复杂度运行效率的对比:

  50000 100000 500000 1000000 4000000
ArrayList 1ms 1ms 4ms 4ms 5ms
LinkedList 4ms 7ms 11ms 13ms 17ms
TreeMap 0ms 0ms 0ms 0ms 0ms

由于再大就内存溢出了,因此只测试到4000000数据。能够看到,数据查找的效率,TreeMap是完胜的,其实再增大数据测试也是同样的,红黑树的数据结构决定了任何一个大于N的最小数据,它都只须要几回至几十次查找就能够查到。

固然,明确一点,有利必有弊,根据我另一次测试获得的结论是, 为了维护红黑树,数据插入效率TreeMap在三种数据结构里面是最差的,且插入要慢上5~10倍 。

Hash值从新计算

服务器节点咱们确定用字符串来表示,好比"192.168.1.1"、"192.168.1.2",根据字符串获得其Hash值,那么另一个重要的问题就是 Hash值要从新计算,这个问题是我在测试String的hashCode()方法的时候发现的,不妨来看一下为何要从新计算Hash值。

  • String的hashCode()方法运算结果查看
  • System.out.println("192.168.0.0:111的哈希值:" + "192.168.0.0:1111".hashCode());
     System.out.println("192.168.0.1:111的哈希值:" + "192.168.0.1:1111".hashCode());
     System.out.println("192.168.0.2:111的哈希值:" + "192.168.0.2:1111".hashCode());
     System.out.println("192.168.0.3:111的哈希值:" + "192.168.0.3:1111".hashCode());
     System.out.println("192.168.0.4:111的哈希值:" + "192.168.0.4:1111".hashCode());

     

    咱们在作集群的时候,集群点的IP以这种连续的形式存在是很正常的。看一下运行结果为:

     

    192.168.0.0:111的哈希值:1845870087
    192.168.0.1:111的哈希值:1874499238
    192.168.0.2:111的哈希值:1903128389
    192.168.0.3:111的哈希值:1931757540
    192.168.0.4:111的哈希值:1960386691

     

    这个就问题大了,[0,2 32 -1]的区间之中,5个HashCode值却只分布在这么小小的一个区间,什么概念?[0,2 32 -1]中有4294967296个数字,而咱们的区间只有122516605,从几率学上讲这将致使97%待路由的服务器都被路由到"192.168.0.1"这个集群点上,简直是糟糕透了!

    另外还有一个很差的地方:规定的区间是非负数,String的hashCode()方法却会产生负数(不信用"192.168.1.0:1111"试试看就知道了)。不过这个问题好解决,取绝对值就是一种解决的办法。

    综上,String重写的hashCode()方法在一致性Hash算法中没有任何实用价值,得找个算法从新计算HashCode。这种从新计算Hash值的算法有不少,好比CRC32_HASH、FNV1_32_HASH、KETAMA_HASH等,其中KETAMA_HASH是默认的MemCache推荐的一致性Hash算法,用别的Hash算法也能够,好比FNV1_32_HASH算法的计算效率就会高一些。

    一致性Hash算法实现版本:不带虚拟节点

    使用一致性Hash算法,尽管加强了系统的伸缩性,可是也有可能致使负载分布不均匀,解决办法就是使用 虚拟节点代替真实节点 ,第一个代码版本,先来个简单的,不带虚拟节点。

    下面来看一下不带虚拟节点的一致性Hash算法的Java代码实现:

     

    1 /**
    2  * 不带虚拟节点的一致性Hash算法
    4  *
    5  */
    6 public class ConsistentHashingWithoutVirtualNode
    7 {
    8     /**
    9      * 待添加入Hash环的服务器列表
    10      */
    11     private static String[] servers = {"192.168.0.0:111", "192.168.0.1:111", "192.168.0.2:111",
    12             "192.168.0.3:111", "192.168.0.4:111"};
    13     
    14     /**
    15      * key表示服务器的hash值,value表示服务器的名称
    16      */
    17     private static SortedMap<Integer, String> sortedMap = 
    18             new TreeMap<Integer, String>();
    19     
    20     /**
    21      * 程序初始化,将全部的服务器放入sortedMap中
    22      */
    23     static
    24     {
    25         for (int i = 0; i < servers.length; i++)
    26         {
    27             int hash = getHash(servers[i]);
    28             System.out.println("[" + servers[i] + "]加入集合中, 其Hash值为" + hash);
    29             sortedMap.put(hash, servers[i]);
    30         }
    31         System.out.println();
    32     }
    33     
    34     /**
    35      * 使用FNV1_32_HASH算法计算服务器的Hash值,这里不使用重写hashCode的方法,最终效果没区别 
    36      */
    37     private static int getHash(String str)
    38     {
    39         final int p = 16777619;
    40         int hash = (int)2166136261L;
    41         for (int i = 0; i < str.length(); i++)
    42             hash = (hash ^ str.charAt(i)) * p;
    43         hash += hash << 13;
    44         hash ^= hash >> 7;
    45         hash += hash << 3;
    46         hash ^= hash >> 17;
    47         hash += hash << 5;
    48         
    49         // 若是算出来的值为负数则取其绝对值
    50         if (hash < 0)
    51             hash = Math.abs(hash);
    52         return hash;
    53     }
    54     
    55     /**
    56      * 获得应当路由到的结点
    57      */
    58     private static String getServer(String node)
    59     {
    60         // 获得带路由的结点的Hash值
    61         int hash = getHash(node);
    62         // 获得大于该Hash值的全部Map
    63         SortedMap<Integer, String> subMap = 
    64                 sortedMap.tailMap(hash);
    65         // 第一个Key就是顺时针过去离node最近的那个结点
    66         Integer i = subMap.firstKey();
    67         // 返回对应的服务器名称
    68         return subMap.get(i);
    69     }
    70     
    71     public static void main(String[] args)
    72     {
    73         String[] nodes = {"127.0.0.1:1111", "221.226.0.1:2222", "10.211.0.1:3333"};
    74         for (int i = 0; i < nodes.length; i++)
    75             System.out.println("[" + nodes[i] + "]的hash值为" + 
    76                     getHash(nodes[i]) + ", 被路由到结点[" + getServer(nodes[i]) + "]");
    77     }
    78 }

     

    能够运行一下看一下结果:

     

    [192.168.0.0:111]加入集合中, 其Hash值为575774686
    [192.168.0.1:111]加入集合中, 其Hash值为8518713
    [192.168.0.2:111]加入集合中, 其Hash值为1361847097
    [192.168.0.3:111]加入集合中, 其Hash值为1171828661
    [192.168.0.4:111]加入集合中, 其Hash值为1764547046

     

[127.0.0.1:1111]的hash值为380278925, 被路由到结点[192.168.0.0:111] [221.226.0.1:2222]的hash值为1493545632, 被路由到结点[192.168.0.4:111] [10.211.0.1:3333]的hash值为1393836017, 被路由到结点[192.168.0.4:111]

看到通过FNV1_32_HASH算法从新计算事后的Hash值,就比原来String的hashCode()方法好多了。从运行结果来看,也没有问题,三个点路由到的都是顺时针离他们Hash值最近的那台服务器上。

 

使用虚拟节点来改善一致性Hash算法

上面的一致性Hash算法实现,能够在很大程度上解决不少分布式环境下很差的路由算法致使系统伸缩性差的问题,可是会带来另一个问题:负载不均。

好比说有Hash环上有A、B、C三个服务器节点,分别有100个请求会被路由到相应服务器上。如今在A与B之间增长了一个节点D,这致使了原来会路由到B上的部分节点被路由到了D上,这样A、C上被路由到的请求明显多于B、D上的,原来三个服务器节点上均衡的负载被打破了。 某种程度上来讲,这失去了负载均衡的意义,由于负载均衡的目的自己就是为了使得目标服务器均分全部的请求 。

解决这个问题的办法是引入虚拟节点,其工做原理是: 将一个物理节点拆分为多个虚拟节点,而且同一个物理节点的虚拟节点尽可能均匀分布在Hash环上 。采起这样的方式,就能够有效地解决增长或减小节点时候的负载不均衡的问题。

至于一个物理节点应该拆分为多少虚拟节点,下面能够先看一张图:

横轴表示须要为每台福利服务器扩展的虚拟节点倍数,纵轴表示的是实际物理服务器数。能够看出,物理服务器不多,须要更大的虚拟节点;反之物理服务器比较多,虚拟节点就能够少一些。好比有10台物理服务器,那么差很少须要为每台服务器增长100~200个虚拟节点才能够达到真正的负载均衡。

一致性Hash算法实现版本2:带虚拟节点

在理解了使用虚拟节点来改善一致性Hash算法的理论基础以后,就能够尝试开发代码了。编程方面须要考虑的问题是:

一、一个真实结点如何对应成为多个虚拟节点?

二、虚拟节点找到后如何还原为真实结点?

这两个问题其实有不少解决办法,我这里使用了一种简单的办法,给每一个真实结点后面根据虚拟节点加上后缀再取Hash值,好比"192.168.0.0:111"就把它变成"192.168.0.0:111&&VN0"到"192.168.0.0:111&&VN4",VN就是Virtual Node的缩写,还原的时候只须要从头截取字符串到"&&"的位置就能够了。

下面来看一下带虚拟节点的一致性Hash算法的Java代码实现:

package cn.chinotan;

import java.util.LinkedList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * @program: test
 * @description: 一致性Hash代码实现
 * @author: xingcheng
 * @create: 2019-04-20 15:34
 **/
public class ConsistentHash {

    /**
     * 服务器地址
     */
    private static String[] servers = {"127.0.0.1:1111", "127.0.0.2:1111", "127.0.0.3:1111",
            "127.0.0.4:1111", "127.0.0.5:1111"};

    /**
     * 节点链表 具备常常上线下线的特色 新增,删除频繁 故采用链表结构
     */
    private static List<String> realNodes = new LinkedList<String>();

    /**
     * 虚拟节点,key表示虚拟节点的hash值,value表示虚拟节点的名称
     */
    private static SortedMap<Integer, String> virtualNodes =
            new TreeMap();

    /**
     * 虚拟节点数量 指一个真实节点对应5个虚拟节点
     */
    private static final int VIRTUAL_NODES = 5;

    static {
        // 先把原始的服务器添加到真实结点列表中
        for (int i = 0; i < servers.length; i++)
            realNodes.add(servers[i]);

        // 再添加虚拟节点,遍历LinkedList使用foreach循环效率会比较高
        for (String str : realNodes) {
            for (int i = 0; i < VIRTUAL_NODES; i++) {
                String virtualNodeName = str + "&&VN" + String.valueOf(i);
                int hash = getHash(virtualNodeName);
                System.out.println("虚拟节点[" + virtualNodeName + "]被添加, hash值为" + hash);
                virtualNodes.put(hash, virtualNodeName);
            }
        }
    }

    /**
     * 使用FNV1_32_HASH算法计算服务器的Hash值
     *
     * @param str
     * @return
     */
    private static int getHash(String str) {
        final int p = 16777619;
        int hash = (int) 2166136261L;
        for (int i = 0; i < str.length(); i++)
            hash = (hash ^ str.charAt(i)) * p;
        hash += hash << 13;
        hash ^= hash >> 7;
        hash += hash << 3;
        hash ^= hash >> 17;
        hash += hash << 5;

        // 若是算出来的值为负数则取其绝对值
        if (hash < 0)
            hash = Math.abs(hash);
        return hash;
    }

    /**
     * 获得应当路由到的结点
     */
    private static String getServer(String node) {
        // 获得带路由的结点的Hash值
        int hash = getHash(node);
        // 获得大于该Hash值的全部Map
        SortedMap<Integer, String> subMap =
                virtualNodes.tailMap(hash);
        // 第一个Key就是顺时针过去离node最近的那个结点
        Integer i = subMap.firstKey();
        // 返回对应的虚拟节点名称,这里字符串稍微截取一下
        String virtualNode = subMap.get(i);
        return virtualNode.substring(0, virtualNode.indexOf("&&"));
    }

    public static void main(String[] args) {
        String[] nodes = {"192.168.1.1:8888", "192.168.1.1:6666", "192.168.1.1:7777"};
        for (int i = 0; i < nodes.length; i++)
            System.out.println("[" + nodes[i] + "]的hash值为" +
                    getHash(nodes[i]) + ", 被路由到结点[" + getServer(nodes[i]) + "]");
    }
}

输出:

虚拟节点[127.0.0.1:1111&&VN0]被添加, hash值为2085384563
虚拟节点[127.0.0.1:1111&&VN1]被添加, hash值为1605685999
虚拟节点[127.0.0.1:1111&&VN2]被添加, hash值为2072955262
虚拟节点[127.0.0.1:1111&&VN3]被添加, hash值为1856019004
虚拟节点[127.0.0.1:1111&&VN4]被添加, hash值为1589545943
虚拟节点[127.0.0.2:1111&&VN0]被添加, hash值为1458771790
虚拟节点[127.0.0.2:1111&&VN1]被添加, hash值为315070177
虚拟节点[127.0.0.2:1111&&VN2]被添加, hash值为1960284216
虚拟节点[127.0.0.2:1111&&VN3]被添加, hash值为691300079
虚拟节点[127.0.0.2:1111&&VN4]被添加, hash值为1067419308
虚拟节点[127.0.0.3:1111&&VN0]被添加, hash值为1442491986
虚拟节点[127.0.0.3:1111&&VN1]被添加, hash值为799875500
虚拟节点[127.0.0.3:1111&&VN2]被添加, hash值为2008417991
虚拟节点[127.0.0.3:1111&&VN3]被添加, hash值为663885024
虚拟节点[127.0.0.3:1111&&VN4]被添加, hash值为332031155
虚拟节点[127.0.0.4:1111&&VN0]被添加, hash值为767137429
虚拟节点[127.0.0.4:1111&&VN1]被添加, hash值为1758330849
虚拟节点[127.0.0.4:1111&&VN2]被添加, hash值为428905736
虚拟节点[127.0.0.4:1111&&VN3]被添加, hash值为679722631
虚拟节点[127.0.0.4:1111&&VN4]被添加, hash值为1459145254
虚拟节点[127.0.0.5:1111&&VN0]被添加, hash值为1832031858
虚拟节点[127.0.0.5:1111&&VN1]被添加, hash值为285365719
虚拟节点[127.0.0.5:1111&&VN2]被添加, hash值为1410730426
虚拟节点[127.0.0.5:1111&&VN3]被添加, hash值为1266752731
虚拟节点[127.0.0.5:1111&&VN4]被添加, hash值为269520663
[192.168.1.1:8888]的hash值为1834873777, 被路由到结点[127.0.0.1:1111]
[192.168.1.1:6666]的hash值为606692241, 被路由到结点[127.0.0.3:1111]
[192.168.1.1:7777]的hash值为1284868523, 被路由到结点[127.0.0.5:1111]

代码运行结果看,每一个点路由到的服务器都是Hash值顺时针离它最近的那个服务器节点

过采起虚拟节点的方法,一个真实结点再也不固定在Hash换上的某个点,而是大量地分布在整个Hash环上,这样即便上线、下线服务器,也不会形成总体的负载不均衡。

相关文章
相关标签/搜索