一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题。一致性哈希修正了CARP使用的简单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用。

        在分布式集群环境当中,机器的添加、删除以及产生故障自动脱离集群这是最基本的功能,如果采用hash(o)%n的算法,在机器数量有变动的时候,以前的数据基本是找不到的。比如最开始3台  hash(o)%3=2  如果增加了一台hash(o)%4=?  结果肯定不会为2,如果使用hash取模,在机器数量增减的时候该问题是无法避免的。为了解决这个问题,就产生了一致性hash算法

 

构造一个2^32的整数环,即0~(2^32-1)的数字空间,形成一个环,起点为0,终点为2^32-1,如下图。按逆时针分布

java一致性hash算法 一致性hash算法 java实现_服务器

假如现在有3个对象o1 o2 o3,使用hash函数计算对应的hash值(0~2^32-1范围内) 我们把它们放在环上。同样计算出3台的机器s1 s2 s3的hash值放入到环上

java一致性hash算法 一致性hash算法 java实现_java一致性hash算法_02

对象为o1 o2 o3   机器s1 s2 s3

现在为对象选择机器:

这里为顺时针,对象o3顺时针查找到s2,即对象o3存储在s2这台机器上的,最后对象o1则存储在s3,如下图

java一致性hash算法 一致性hash算法 java实现_i++_03

 

当其中一个服务器挂掉了,比如s3挂了。o1被重新分配到了s2

java一致性hash算法 一致性hash算法 java实现_java一致性hash算法_04

 

如果增加了机器,同理也会进行相应的负载

java代码的实现:

//待添加入Hash环的服务器列表
    private static String[] servers = {"192.168.0.0:111", "192.168.0.1:111",
            "192.168.0.2:111", "192.168.0.3:111", "192.168.0.4:111"};

    //key表示服务器的hash值,value表示服务器
    private static SortedMap<Integer, String> sortedMap = new TreeMap<Integer, String>();

    //程序初始化,将所有的服务器放入sortedMap中
    static {
        for (int i = 0; i < servers.length; i++) {
            int hash = getHash(servers[i]);
            System.out.println("[" + servers[i] + "]加入集合中, 其Hash值为" + hash);
            sortedMap.put(hash, servers[i]);
        }
        System.out.println();
    }

    //得到应当路由到的结点
    private static String getServer(String key) {
        //得到该key的hash值
        int hash = getHash(key);
        //得到大于该Hash值的所有Map
        SortedMap<Integer, String> subMap = sortedMap.tailMap(hash);
        if (subMap.isEmpty()) {
            //如果没有比该key的hash值大的,则从第一个node开始
            Integer i = sortedMap.firstKey();
            //返回对应的服务器
            return sortedMap.get(i);
        } else {
            //第一个Key就是顺时针过去离node最近的那个结点
            Integer i = subMap.firstKey();
            //返回对应的服务器
            return subMap.get(i);
        }
    }

    //使用FNV1_32_HASH算法计算服务器的Hash值,这里不使用重写hashCode的方法,最终效果没区别
    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;
    }

    public static void main(String[] args) {
        String[] keys = {"太阳", "月亮", "星星"};
        for (int i = 0; i < keys.length; i++) {
            System.out.println("[" + keys[i] + "]的hash值为" + getHash(keys[i])
                    + ", 被路由到结点[" + getServer(keys[i]) + "]");
        }
    }

[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

[太阳]的hash值为1977106057, 被路由到结点[192.168.0.1:111]
[月亮]的hash值为1132637661, 被路由到结点[192.168.0.3:111]
[星星]的hash值为880019273, 被路由到结点[192.168.0.3:111]

有重复服务器使用

虚拟节点:

虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一实际个节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列

个人理解虚拟节点就是为了提高服务器命中率,分布得更均匀

如下图:

java一致性hash算法 一致性hash算法 java实现_i++_05

上图的s11 s22 s33分别是不同机器的虚拟节点,可以很多个,分布在hash环上,还是以hash函数的hash值来确定数字空间

带虚拟节点的:

//待添加入Hash环的服务器列表
    private static String[] servers = {"192.168.0.0:111", "192.168.0.1:111",
            "192.168.0.2:111", "192.168.0.3:111", "192.168.0.4:111"};

    //真实节点列表
    private static List<String> realNodes = new LinkedList<>();

    //虚拟节点列表
    private static SortedMap<Integer, String> sortedMap = new TreeMap<Integer, String>();

    private static final int NUM_HOST = 5;

    //程序初始化,将所有的服务器放入sortedMap中
    static {
        //添加真实节点
        for (int i = 0; i < servers.length; i++) {
            realNodes.add(servers[i]);
        }
        //添加虚拟节点
        for (String str : realNodes) {
            for (int i = 1; i <= NUM_HOST; i++) {
                String nodeName = str + "VM" + String.valueOf(i);
                int hash = getHash(nodeName);
                sortedMap.put(hash, nodeName);
                System.out.println("虚拟节点hash:" + hash + "【" + nodeName + "】放入");
            }
        }
    }

    //得到应当路由到的结点
    private static String getServer(String key) {
        //得到该key的hash值
        int hash = getHash(key);
        //得到大于该Hash值的所有Map
        String host;
        SortedMap<Integer, String> subMap = sortedMap.tailMap(hash);
        if (subMap.isEmpty()) {
            //如果没有比该key的hash值大的,则从第一个node开始
            Integer i = sortedMap.firstKey();
            //返回对应的服务器
            host = sortedMap.get(i);
        } else {
            //第一个Key就是顺时针过去离node最近的那个结点
            Integer i = subMap.firstKey();
            //返回对应的服务器
            host= subMap.get(i);
        }
        if (StringUtils.isNotBlank(host)) {
            String realHost = host.substring(0,host.indexOf("VM"));
            System.out.println(realHost);
            return realHost;
        }
        return null;
    }

    //使用FNV1_32_HASH算法计算服务器的Hash值
    private static int getHash(String str) {
//        int hash = str.hashCode();
        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;
    }

    public static void main(String[] args) {
        String[] keys = {"天下", "无敌", "的我"};
        for (int i = 0; i < keys.length; i++) {
            System.out.println("[" + keys[i] + "]的hash值为" + getHash(keys[i]) + ", 被路由到结点[" + getServer(keys[i]) + "]");
        }
    }

虚拟节点hash:203600595【192.168.0.0:111VM1】放入
虚拟节点hash:533864743【192.168.0.0:111VM2】放入
虚拟节点hash:283014282【192.168.0.0:111VM3】放入
虚拟节点hash:112805468【192.168.0.0:111VM4】放入
虚拟节点hash:2047670539【192.168.0.0:111VM5】放入
虚拟节点hash:519865065【192.168.0.1:111VM1】放入
虚拟节点hash:1154856732【192.168.0.1:111VM2】放入
虚拟节点hash:1253620420【192.168.0.1:111VM3】放入
虚拟节点hash:271923136【192.168.0.1:111VM4】放入
虚拟节点hash:954403340【192.168.0.1:111VM5】放入
虚拟节点hash:1573046012【192.168.0.2:111VM1】放入
虚拟节点hash:1216691265【192.168.0.2:111VM2】放入
虚拟节点hash:1427550122【192.168.0.2:111VM3】放入
虚拟节点hash:563194059【192.168.0.2:111VM4】放入
虚拟节点hash:1191243314【192.168.0.2:111VM5】放入
虚拟节点hash:449809950【192.168.0.3:111VM1】放入
虚拟节点hash:1517398358【192.168.0.3:111VM2】放入
虚拟节点hash:1061446677【192.168.0.3:111VM3】放入
虚拟节点hash:1711703068【192.168.0.3:111VM4】放入
虚拟节点hash:888576886【192.168.0.3:111VM5】放入
虚拟节点hash:300861392【192.168.0.4:111VM1】放入
虚拟节点hash:185778140【192.168.0.4:111VM2】放入
虚拟节点hash:70846991【192.168.0.4:111VM3】放入
虚拟节点hash:20072546【192.168.0.4:111VM4】放入
虚拟节点hash:294895133【192.168.0.4:111VM5】放入
192.168.0.0:111
[天下]的hash值为1815790460, 被路由到结点[192.168.0.0:111]
192.168.0.3:111
[无敌]的hash值为705568906, 被路由到结点[192.168.0.3:111]
192.168.0.4:111
[的我]的hash值为2055637786, 被路由到结点[192.168.0.4:111]

这样分布得更均匀,每个负载比较平均