手动实现一致性 Hash 算法

家喻户晓的一致性 Hash 算法是解决数据分散布局或者说分布式环境下系统伸缩性差的优质解,本文旨在使用 Java 语言手动实现一套该算法。java


1、背景

最简单的一个应用场景即是缓存,当单机缓存量过大时须要分库,而后根据相关信息进行 hash 取模运算到指定的机器上去,好比 index = hash(ip) % N。node

可是当增长或者减小节点的时候,因为上述公式的 N 值是有变化的,因此绝大部分,甚至说全部的缓存都会失效,对于这种场景最直接的解决办法即是使用一致性 hash 算法算法

2、一致性 Hash 算法简介

一、简单的一致性 Hash

关于一致性 Hash 算法,简单的抽象描述就是一个圆环,而后上面均匀布局了 2^32 个节点,好比 [0,1,2,4,8…],而后将咱们的机器节点散列在这个圆环上,至于散列的规则,可使用 hash(ip) 或者 hash(域名)。数组

hash 环
hash 环

当寻找数据的时候,只须要将目标数据的key散列在这个环上,而后进行顺时针读取最近的一个机器节点上的数据便可。缓存

以下图的简单版本,假如总共有3个数据节点(A、B、C),当须要查找的数据的key经计算在A和B之间,则顺时针找,便找到了节点B。服务器

数据查找
数据查找

最大的优势是:仍是以上面的案例为背景,当节点B宕了以后,顺时针便找到了C,这样,影响的范围仅仅是A和B之间的数据,对于其余的数据是不影响的。数据结构

二、虚拟节点

可是在散列数据节点的时候,紧凑性会受 hash 算法的影响,好比A、B、C三个数据服务器,在 hash 计算后散列在 一、二、4三个节点上,这样就会由于太密集而失去**平衡性。**好比此时咱们要查找的数据的key通过 hash 运算以后,大几率是出如今4和1之间的,即在C以后,那样的话顺时针查找便会找到A,那么A服务器便承载了几乎全部的负载,这就失去了该算法的意义。分布式

此时虚拟节点便出现了,好比上述的三台服务器各虚拟分裂出1各虚拟节点(A一、B一、C1),那么这样即可以在必定程度上解决一致性hash的平衡性问题。ide

虚拟节点A一、B一、C1
虚拟节点A一、B一、C1

3、数组简陋版

一、思路

简单描述下思路:其实就是使用一个数组去存储全部的节点信息,存完以后须要手动排序一下,由于是有序的,因此取的时候就从 index 为0开始挨个对比节点的 hash 值,直到找到一个节点的 hash 值是比咱们的目标数据的 hash(key) 大便可,不然返回第一个节点的数据。函数

二、代码其实很简单,直接撸:

package  com.jet.mini.utils;

import java.util.Arrays;

/** * @ClassName: SortArrayConsistentHash * @Description: 初代数组实现的一致性哈数算法 * @Author: Jet.Chen * @Date: 2019/3/19 23:11 * @Version: 1.0 **/

public  class SortArrayConsistentHash {

    /**      * 最为核心的数据结构      */
    private  Node[]  buckets;

    /**      * 桶的初始大小      */
    private  static  final  int  INITIAL_SIZE  =  32;

    /**      * 当前桶的大小      */
    private  int  length  =  INITIAL_SIZE;

    /**      * 当前桶的使用量      */
    private  int  size  =  0;

    public SortArrayConsistentHash(){

        buckets  =  new  Node[INITIAL_SIZE];

    }

    /**      * 指定数组长度的构造      */

    public SortArrayConsistentHash(int length){

        if  (length  <  32)  {
            buckets  =  new  Node[INITIAL_SIZE];
        }  else  {
            this.length  =  length;
            buckets  =  new  Node[length];
        }

    }

    /**      * @Description: 写入数据      * @Param: [hash, value]      * @return: void      * @Author: Jet.Chen      * @Date: 2019/3/19 23:38      */
    public void add(long hash, String value){
        // 大小判断是否须要扩容
        if  (size  ==  length)  reSize();
        Node node  =  new  Node(value,  hash);
        buckets[++size]  =  node;
    }

    /**      * @Description: 删除节点      * @Param: [hash]      * @return: boolean      * @Author: Jet.Chen      * @Date: 2019/3/20 0:24      */
    public boolean del(long hash) {
        if  (size  ==  0)  return  false;
        Integer  index  =  null;
        for  (int  i  =  0;  i  <  length;  i++)  {
            Node node  =  buckets[i];
            if  (node  ==  null)  continue;
            if  (node.hash  ==  hash)  index  =  i;
        }

        if  (index  !=  null)  {
            buckets[index]  =  null;
            return  true;
        }

        return  false;
    }

    /**      * @Description: 排序      * @Param: []      * @return: void      * @Author: Jet.Chen      * @Date: 2019/3/19 23:48      */

    public void sort() {
        // 此处的排序不须要关注 eqals 的状况
        Arrays.sort(buckets,  0,  size,  (o1,  o2)  ->  o1.hash  >  o2.hash  ?  1  :  -1);
    }

    /**      * @Description: 扩容      * @Param: []      * @return: void      * @Author: Jet.Chen      * @Date: 2019/3/19 23:42      */
    public void reSize() {
        // 扩容1.5倍
        int  newLength  =  length  >>  1  +  length;
        buckets  =  Arrays.copyOf(buckets,  newLength);
    }

    /**      * @Description: 根据一致性hash算法获取node值      * @Param: [hash]      * @return: java.lang.String      * @Author: Jet.Chen      * @Date: 2019/3/20 0:16      */

    public String getNodeValue(long hash) {
        if  (size  ==  0)  return  null;
        for  (Node bucket  :  buckets)  {
            // 防止空节点
            if  (bucket  ==  null)  continue;
            if  (bucket.hash  >=  hash)  return  bucket.value;
        }

        // 防止循环没法尾部对接首部
        // 场景:仅列出node的hash值,[null, 2, 3...],可是寻求的hash值是4,上面的第一遍循环很显然没能找到2这个节点,全部须要再循环一遍
        for  (Node bucket  :  buckets)  {
            if  (bucket  !=  null)  return  bucket.value;
        }

        return  null;

    }

    /**      * node 记录了hash值和原始的IP地址      */
    private  class Node {
        public  String  value;
        public  long  hash;
        public Node(String value, long hash) {
            this.value  =  value;
            this.hash  =  hash;
        }

        @Override
        public String toString() {
            return  "Node{hash="+hash+", value="+value+"}";
        }

    }

}
复制代码

三、弊端

① 排序算法:上面直接使用 Arrays.sort() ,即 TimSort 排序算法,这个值得改进;

② hash 算法:上文没有说起 hash 算法,须要改进;

③ 数据结构:上文使用的是数组,可是须要手动进行排序,优势是插入速度尚可,可是扩容不便,并且须要手动排序,排序的时机也不定,须要改进;

④ 虚拟节点:没有考虑虚拟节点,须要改进。

4、TreeMap 进阶版

上文的实现既然有弊端,那就操刀改进之:

① 数据结构:咱们可使用 TreeMap 数据结构,优势是该数据结构是有序的,无需再排序,并且该数据结构中有个函数叫 tailMap,做用是获取比指定的 key 大的数据集合;

② hash 算法:此处咱们使用 FNV1_32_HASH 算法,该算法证明下来散列分布比较均匀,hash 碰撞尚且 ok;

③ 虚拟节点:咱们暂且设置每一个节点锁裂变的虚拟节点数量为10。

代码也不难,也是直接撸:

package  com.jet.mini.utils;

import java.util.SortedMap;
import java.util.TreeMap;

/** * @ClassName: TreeMapConsistentHash * @Description: treeMap 实现的进化版一致性hash * @Author: Jet.Chen * @Date: 2019/3/20 20:44 * @Version: 1.0 **/
public  class TreeMapConsistentHash {

    /**      * 主要数据结构      */
    private  TreeMap<Long,  String>  treeMap  =  new  TreeMap<>();

    /**      * 自定义虚拟节点数量      */
    private  static  final  int  VIRTUAL_NODE_NUM  =  10;

    /**      * 普通的增长节点      */
    @Deprecated
    public void add (String key, String value) {
        long  hash  =  hash(key);
        treeMap.put(hash,  value);
    }

    /**      * 存在虚拟节点      */
    public void add4VirtualNode(String key, String value) {
        for  (int  i  =  0;  i  <  VIRTUAL_NODE_NUM;  i++)  {
            long  hash  =  hash(key  +  "&&VIR"  +  i);
            treeMap.put(hash,  value);
        }
        treeMap.put(hash(key),  value);
    }

    /**      * 读取节点值      * @param key      * @return      */
    public String getNode(String key) {
        long  hash  =  hash(key);
        SortedMap<Long,  String>  sortedMap  =  treeMap.tailMap(hash);
        String  value;
        if  (!sortedMap.isEmpty())  {
            value  =  sortedMap.get(sortedMap.firstKey());
        }  else  {
            value  =  treeMap.firstEntry().getValue();
        }

        return  value;
    }

    /**      * 使用的是 FNV1_32_HASH      */
    public long hash(String key) {
        final  int  p  =  16777619;
        int  hash  =  (int)2166136261L;
        for(int  i  =  0;  i  <  key.length();  i++)  {
            hash  =  (hash  ^  key.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;
    }

}
复制代码

5、其余

一、虚拟节点的数量建议:

虚拟节点数量和服务器数量关系(待考量)
虚拟节点数量和服务器数量关系(待考量)

看上图,X 轴是虚拟节点数量,Y 轴是服务器数量,很显然,服务器越多,建议的虚拟节点数量也就越少。

原文地址:www.jetchen.cn/consistent-…

相关文章
相关标签/搜索