一致性哈希算法

一致性哈希算法

一致性哈希算法(Consistent Hasing,以下简称CH)是一种特殊的哈希算法,使用CH的哈希表扩容的时候,平均只有K/n个关键字需要被重新映射(或者移动),这里的K是关键字的数量,n指的是哈希表的槽位(slot)。

而传统的哈希表在扩容的时候,几乎所有的关键字需要被重新映射(或移动)。

传统的哈希表的工作流程如下:
设一个哈希表有nslot,每个关键字key通过哈希函数得到一个哈希值h,然后通过取模h%n得到所在的slot位置。

当哈希表扩容的时候,上述的n发生了变化,因此原来的关键字和slot之间的映射关系发生了变化,现在需要重新建立这种关系。当这个哈希表特别大的时候,涉及到的关键字特别多,这种扩容带来的代价十分巨大。

基于传统哈希表的这个局限,一致性哈希算法通过将映射后的空间组织成一个哈希环Circle,每个环上有多个结点Node,因此结点将映射后的空间划分成了多段,当新增或者删除结点的时候,只需要重新映射某一段的数据即可。

CH常常作为一种负载均衡算法,在很多地方都得到了应用。

关键字的定位

示意图如下:

上述示意图描述了一个具有8个节点的哈希环,当一个关键字需要定位到某个节点的时候,首先根据哈希函数计算关键字的哈希值h,然后这个哈希值必定落在某个区间里面,顺时针找到第一个大于该哈希值的节点,将关键字存储到该结点。

节点删除

当由于主机宕机、负载过高等原因,需要暂时将某个节点从环中删除,此时CH不需要改变所有的数据映射的关系,而只是修改部分数据的映射关系即可。

示意图如下:

这里写图片描述

当节点sn1被删除的时候,哈希值落在sn0到sn1之间的关键字在定位的时候会定位到sn2,因此需要将sn1的数据拷贝到sn2节点上。

节点增加

同节点删除类似,示意图如下:

当在sn1和sn2之间增加了节点sn8,需要将sn2的数据迁移到sn8节点上。

虚拟节点

当哈希环上的节点的数量比较少的时候,可能会出现节点在环上分布不均匀,例如出现以下这种情况:

这种情况必然会造成大量的请求转发给服务器1,造成服务器1不堪重负,而服务器2却无所事事,这就是所谓的负载不均衡。
因此在环上节点分布不均匀的时候,可以增加若干个虚拟节点来有效的解决这个负载不均衡的问题。

于是可以增加“Server 1#1”、“Server 1#2”、“Server 1#3”、“Server 2#1”、“Server 2#2”、“Server 2#3”,于是形成六个虚拟节点,这样在真实节点少的情况下,可以将空间划分得更加均匀一点。

代码实现


public class ConsistentHash<T> {
    interface HashFunction {
        int hash(String s);
    }

    private int numerOfVirtualNode;

    private SortedMap<Integer, T> hashCircle = new TreeMap<>();
    private HashFunction hashFunction;

    public ConsistentHash (int numerOfVirtualNode, Collection<T> nodes) {
        this(null, numerOfVirtualNode, nodes);
    }


    public ConsistentHash (HashFunction hf, int numerOfVirtualNode, Collection<T> nodes) {
        this.numerOfVirtualNode = numerOfVirtualNode;
        this.hashFunction = hf;
        for (T node : nodes) {
            add(node);
        }
    }

    /**
     * 增加一个节点
     * @param node
     */
    public void add(T node) {
        // TODO Auto-generated method stub
        for (int i = 0; i < numerOfVirtualNode; i++) {
            hashCircle.put(hashFunction.hash(node.toString() + i), node);
        }
        //这里省略了增加节点以后,数据迁移的操作。
    }

    /**
     * 删除一个节点
     * @param node
     */
    public void remove(T node) {
        for (int i = 0; i < numerOfVirtualNode; i++) {
            hashCircle.remove(hashFunction.hash(node.toString() + i));
        }
        //这里省略了删除节点以后,数据迁移的操作。
    }

    /**
     * 顺时针寻找第一个大于key的hash值的节点
     * @param key
     * @return
     */
    public T get(Object key) {
        if (hashCircle.isEmpty()) {
            return null;
        }
        int hash = hashFunction.hash(key.toString());
        if (!hashCircle.containsKey(hash)) {
            SortedMap<Integer, T> tailMap = hashCircle.tailMap(hash);
            hash = tailMap.isEmpty() ? hashCircle.firstKey() : tailMap.firstKey();
        }
        return hashCircle.get(hash);
    }

    //default hash function
    static class MD5HashFunction implements HashFunction {
        MessageDigest md5 = null;
        public MD5HashFunction() {
            // TODO Auto-generated constructor stub
            try {
                md5 = MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        @Override
        public int hash(String s) {
            // TODO Auto-generated method stub
            md5.reset();
            byte[] res = md5.digest(s.getBytes());
            int hash = ((res[0] & 0xFF) << 24) | ((res[1] & 0xFF) << 16)| ((res[2] & 0xFF) << 8) | ((res[3] & 0xFF)); 
            return hash;
        }
    }
}

references

  1. http://blog.51cto.com/alanwu/1431397
  2. https://en.wikipedia.org/wiki/Consistent_hashing
  3. http://www.cnblogs.com/moonandstar08/p/5405991.html
  4. https://web.archive.org/web/20120605030524/http://weblogs.java.net/blog/tomwhite/archive/2007/11/consistent_hash.html
posted @ 2018-04-10 16:28  Spground  阅读(175)  评论(0编辑  收藏  举报