最近一段时间忙着两个重大项目的发布,好久没有更新文章了。一致性哈希其实并不复杂,在各大面试中都能接触到。一般的套路就是从Hashmap开始问起其中的hashcode,到一致性哈希的思路,今天我们从实现的角度看一个简单的实现,也让大家有个更直观的印象。

    

       在强一致性的分布式选举算法Raft中使用leader-follower机制实现了共识协商,但写请求只能限制在leader节点上处理,导致了集群的处理性能类似于单机,那么请求量的增加,集群的性能可能就扛不住了,会造成系统过载和服务不可用,这时可以考虑使用哈希算法。


1、哈希算法

     关于哈希算法,我曾经在《手把手写一个集合框架(九)哈希表》中介绍过哈希算法的原理,其最终根据计算得到一个int型的数据,这个数值就是hashcode。这个int型的hashcode也是下面我们说的一致性哈希为什么会在2的32次方减1这个圆环上分布节点和数据的原因。


     哈希算法是计算数据的哈希值,然后根据节点的个数作为哈希桶,将该数据的哈希值进行取余操作,然后存放到该节点中。等查找该数据的时候,需要根据该数据的哈希值得到存放的节点,然后获取该节点下的数据。这里,可以把节点想象成分布式系统下的单台服务器。数据存放在某节点就代表存放在某台机器。


      这里,我们以一个Key-Value数据为例,类似于Redis集群那样存放。这里假设定义的节点DataObj类如下:

public class DataObj {    String key;    String value;    public DataObj(String key, String value) {        this.key = key;        this.value = value;    }    //此处省略hashcode方法和toString方法}

    我们定义一下机器的节点Node类,用于存储上面的数据节点。简单起见,我们只实现一下存和取方法。

public class Node {    HashMap<Integer,DataObj> hashmap;    String NodeName;//NodeName的作用在实现虚拟节点有用    public Node(String nodeName) {        NodeName = nodeName;        hashmap = new HashMap<>();    }     //存放数据    public void putDataObj(DataObj obj){        hashmap.put(obj.hashCode(),obj);        return;    }    //获取数据      public DataObj getData(DataObj obj){      return hashmap.get(obj.hashCode());    }}

        接下来,我们实现一个类NodeArray存放所有节点,通过该类存放和获取数据,相当于访问集群的意思了。

public class NodeArray {    public static final  int MAX_NODE= 1024;    Node[] nodes = new Node[MAX_NODE];    int size = 0;    public NodeArray() {    }
   public void addNode(Node node){        if(size>MAX_NODE) return;        nodes[size++] = node;    }
   DataObj get(DataObj obj) {        int index = obj.hashCode() % size;        return nodes[index].getData(obj);    }
   void put(DataObj obj) {        int index = obj.hashCode() % size;        nodes[index].putDataObj(obj);    }}

       下面我们测试看看,假如我们创建了3个节点,并存放了5个数据。最后再逐一访问这些数据节点,这些数据可以正常查询到。

从分布式看一致性哈希算法_java

    然后,我们新增一个节点或删除一个节点,此时已经无法获取到节点了。

从分布式看一致性哈希算法_java_02


2、一致性哈希算法

     上面的问题出现在增加或减少节点的情况下,这就类似于某台机器故障或扩容了一台机器,此时数据访问基本全部失效,需要重新rehash全部数据。而一致性哈希算法则可以有效减少数据迁移工作。那么一致性哈希算法如何解决的呢?


      一致性哈希首先将节点也映射到这个环上面,这个环就是hashcode的大小,当通过节点的key去查找节点的时候,先找到最近的Node,然后再获取值。

public class ConsistNodeArray {    /** 按照键排序*/    TreeMap<Integer, Node> nodes = new TreeMap<>();    void addNode(Node node) {        nodes.put(node.hashCode(), node);    }
   void put(DataObj obj) {       int objHashcode = obj.hashCode();        Node node = nodes.get(objHashcode);        if (node != null) {            node.putDataObj(obj);            return;        }     // 找到比给定 key 大的集合     SortedMap<Integer, Node> tailMap = nodes.tailMap(objHashcode);     // 找到最小的节点     int nodeHashcode = tailMap.isEmpty() ?                    nodes.firstKey() : tailMap.firstKey();     nodes.get(nodeHashcode).putDataObj(obj);    }
   DataObj get(DataObj obj) {        Node node = nodes.get(obj.hashCode());        if (node != null) {            return node.getData(obj);        }      // 找到比给定 key 大的集合      SortedMap<Integer,Node> tailMap=nodes.tailMap(obj.hashCode());        // 找到最小的节点     int nodeHashcode = tailMap.isEmpty() ?                  nodes.firstKey():tailMap.firstKey();     return nodes.get(nodeHashcode).getData(obj);    }}

      此时如果不新增节点的话,测试结果可以全部查询到数据。如果新增2个节点,也能获取大部分数据。

从分布式看一致性哈希算法_java_03


      这里需要注意的是,新增或减少节点,肯定需要做部分数据迁移的,只是这个迁移量很小,有一些专业的测试结果显示:对于 1000 万 key 的 3 节点 KV 存储,如果我们增加 1 个节点,变为 4 节点集群,只需要迁移 24.3% 的数据。如果原有节点数量越多,其实迁移量更少。当然了,如果这里引入虚拟节点,那么迁移量更低。


      至于虚拟节点的实现,可以在创建Node的时候指定名称为NodeName-A,NodeName-B之类的,实际节点是同一个Node即可,这里不再赘述。


3、总结

     一致性哈希算法通过两次哈希计算,不直接寻找映射的节点,而是找到距离当前数据哈希值最近的节点Node,然后将数据存储到该节点上。当发生节点Node故障或新增节点时,也只是影响这部分区域的节点。

     对于要是想数据更均匀的分布在一致性哈希算法上面,可以引入虚拟节点的方法解决。