一致性hash KetamaHash代码实现(分布式路由算法)及原理分析

162 阅读4分钟

KetamaHash代码实现一致性hash(分布式路由算法)及原理分析

一致性hash是什么

一致性hash作为常见的分布式路由算法,可以很好的实现服务端的负载均衡。用户在请求时通过分布式路由算法打到不同的服务器上,可以保证用户粘性的同时有很好的伸缩性,服务器的增删节点并不会造成大规模的数据移动。

一致性hash原理

一句话,散列函数,hash越剧烈,散列分布越均衡,这就是一致性hash的本质。

==简单的hash路由:hash(key)%节点数== 这种硬hash的方式存在很多问题,如果对节点进行增删,会导致大量的缓存命中不了,假设有三个节点的集群,在增加一个节点之后,将会有3/4的数据会失效,而失效的比例在节点数很多的情况下比例会更大。

==可能会导致缓存雪崩,大量数据压入数据库==

一致性hash

一致性hash主要是通过更多的虚拟节点,来控制整个hash散列分布更加均衡,就可以缓解由于增删机器导致的大量缓存失效问题,而仅仅会失效一小部分缓存。这也就是上面说的不会造成大规模的数据移动。 那么虚拟节点更多就散列分布更均衡了吗,这就涉及到最根本的问题,hash算法,一致性hash推荐使用KetamaHash算法来实现散列分布,此文也通过该hash算法来实现更均衡的hash分布。

直接上代码

==下面将写两种hash算法,由于fnv1_32_hash在csdn上搜索一致性hash基本都是基于此实现,所以一个KetamaHash一个fnv1_32_hash,后续会测试两种hash算法的效果,其实还是KetamaHash效果好==

首先是KetamaHash算法,通过其getHash(key)来获得hash值

public class KetamaHash {
    private static MessageDigest md5Digest;
    static {
        try {
            md5Digest = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException var1) {
            throw new RuntimeException("MD5 not supported", var1);
        }
    }
    public int getHash(String origin) {
        byte[] bKey = computeMd5(origin);
        long rv = (long)(bKey[3] & 255) << 24 | (long)(bKey[2] & 255) << 16 | (long)(bKey[1] & 255) << 8 | (long)(bKey[0] & 255);
        return (int)(rv & 4294967295L);
    }

    private static byte[] computeMd5(String k) {
        MessageDigest md5;
        try {
            md5 = (MessageDigest)md5Digest.clone();
        } catch (CloneNotSupportedException var3) {
            throw new RuntimeException("clone of MD5 not supported", var3);
        }

        md5.update(k.getBytes());
        return md5.digest();
    }
}

fnv1_32_hash实现(百度的)

    public static int getHashCode(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;
    }

一致性hash的代码(ketamaHash.getHash(virnode)计算hash)

public class hashtest {
    //物理节点
    List<String> realnodes = new ArrayList<String>();

    //虚拟节点的数量(每个真实节点虚拟出来的数量总和大概在1000-2000最佳)
    //默认100
    private int virNodeNum = 100;

    //定义一个存放hash的TreeMap
    private SortedMap<Integer,String> hashTree = new TreeMap<Integer, String>();

    //创建类初始化指定每台物理节点的虚拟节点数量
    public hashtest(int virNodeNum){
        this.virNodeNum = virNodeNum;
    }
    KetamaHash ketamaHash = new KetamaHash();

    //添加一个服务器节点
    public void addNode(String node)
    {
        //物理节点列表添加一个
        realnodes.add(node);
        //开始虚拟物理节点
        String virnode = null;
        List<String> virnodes = new ArrayList<String>();
        for (int i = 0; i <virNodeNum ; i++) {
            //虚拟节点的名字(用于hash)
            virnode = node+"vir/*/"+i;
            //添加到虚拟节点列表中
            virnodes.add(virnode);
            //ketama计算hash值
            int virhash = Math.abs(ketamaHash.getHash(virnode));
            //int virhash = getHashCode(virnode);
            //保存到hash树
            hashTree.put(virhash,node);
        }
    }

    //删除一个节点
    public void removenode(String node) {
        realnodes.remove(node);
        Iterator<Map.Entry<Integer, String>> keys = hashTree.entrySet().iterator();
        while (keys.hasNext())
        {
            Map.Entry<Integer, String> next = keys.next();
            if(next.getValue().equals(node))
            {
                keys.remove();
            }
        }
    }

    //路由节点(获取服务器node)
    public String getNode(String key)
    {
        int virhash = Math.abs(ketamaHash.getHash(key));
        //int virhash = getHashCode(key);
        //获取大于等于该key的子map(hash环顺时针)
        SortedMap<Integer,String> sortedMap = hashTree.tailMap(virhash);
        if (sortedMap.isEmpty())
        {
            return  hashTree.get(hashTree.firstKey());
        }
        else
        {
            return  sortedMap.get(sortedMap.firstKey());
        }
    }
    
}

测试效果

一、使用ketamaHash:

    public static void main(String[] args) {

        hashtest sda = new hashtest(500);
        sda.addNode("192.168.2.3");
        sda.addNode("192.168.2.4");
        sda.addNode("192.168.2.5");
        sda.addNode("192.168.2.6");
        sda.addNode("192.168.2.6");
        List<String> res = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {

            String node = sda.getNode("hello" + i);
            res.add(node);
        }
        Map<String,Integer> map = new HashMap();
        for(String item: res){
            if(map.containsKey(item)){
                map.put(item, map.get(item) + 1);

            }else{
                map.put(item, new Integer(1));

            }

        }

        Iterator keys = map.keySet().iterator();

        while(keys.hasNext()){
            String key = (String) keys.next();

            System.out.println("服务器地址"+key + ":" +"命中数量"+ map.get(key).intValue() + ", ");

        }
    }

上述代码什么意思呢,就是每个物理节点虚拟出500个节点来进行操作。理论上虚拟节点越多,分布越均匀。

输出结果:

在这里插入图片描述

使用fnv1_32_hash

在这里插入图片描述

==看到结果对比会发现使用fnv1_32_hash效果确实还是不如ketamahash分布均匀,命中数量最大306最少211差距还是挺大的。==