自己實現一個一致性 Hash 演算法

莫那·魯道發表於2019-03-04

前言

在前文分散式理論(八)—— Consistent Hash(一致性雜湊演算法)中,我們討論了一致性 hash 演算法的原理,並說了,我們會自己寫一個簡單的演算法。今天就來寫一個。

普通 hash 的結果

先看看普通 hash 怎麼做。

首先,需要快取節點物件,快取中的儲存物件,還有一個快取節點集合,用於儲存有效的快取節點。

  1. 實際儲存物件,很簡單的一個類,只需要獲取他的 hash 值就好:
  static class Obj {
    String key;
    Obj(String key) {
      this.key = key;
    }
    @Override
    public int hashCode() {
      return key.hashCode();
    }
    @Override
    public String toString() {
      return "Obj{" +
          "key=`" + key + ``` +
          `}`;
    }
  }
複製程式碼
  1. 快取節點物件,用於儲存實際物件:
  static class Node {

    Map<Integer, Obj> node = new HashMap<>();
    String name;

    Node(String name) {
      this.name = name;
    }

    public void putObj(Obj obj) {
      node.put(obj.hashCode(), obj);
    }

    Obj getObj(Obj obj) {
      return node.get(obj.hashCode());
    }

    @Override
    public int hashCode() {
      return name.hashCode();
    }
  }
複製程式碼

也很簡單,內部使用了一個 map 儲存節點。

  1. 快取節點集合,用於儲存有效的快取節點:
 static class NodeArray {

    Node[] nodes = new Node[1024];
    int size = 0;

    public void addNode(Node node) {
      nodes[size++] = node;
    }

    Obj get(Obj obj) {
      int index = obj.hashCode() % size;
      return nodes[index].getObj(obj);
    }

    void put(Obj obj) {
      int index = obj.hashCode() % size;
      nodes[index].putObj(obj);
    }
  }

複製程式碼

內部一個陣列,取資料時,通過取餘機器數量獲取快取節點,再從節點中取出資料。

  1. 測試:當增減節點時,還能不能找到原有資料:
 /**
   * 驗證普通 hash 對於增減節點,原有會不會出現移動。
   */
  public static void main(String[] args) {

    NodeArray nodeArray = new NodeArray();

    Node[] nodes = {
        new Node("Node--> 1"),
        new Node("Node--> 2"),
        new Node("Node--> 3")
    };

    for (Node node : nodes) {
      nodeArray.addNode(node);
    }

    Obj[] objs = {
        new Obj("1"),
        new Obj("2"),
        new Obj("3"),
        new Obj("4"),
        new Obj("5")
    };

    for (Obj obj : objs) {
      nodeArray.put(obj);
    }

    validate(nodeArray, objs);
  }
複製程式碼
  private static void validate(NodeArray nodeArray, Obj[] objs) {
    for (Obj obj : objs) {
      System.out.println(nodeArray.get(obj));
    }

    nodeArray.addNode(new Node("anything1"));
    nodeArray.addNode(new Node("anything2"));

    System.out.println("========== after  =============");

    for (Obj obj : objs) {
      System.out.println(nodeArray.get(obj));
    }
  }

複製程式碼

測試步驟如下:

  1. 向集合中新增 3 個節點。
  2. 叢集 中新增 5 個物件,這 5 個物件會根據 hash 值雜湊到不同的節點中。
  3. 列印 未增減前 的資料。
  4. 列印 增加 2 個節點 後資料,看看還能不能訪問到資料。

結果:

自己實現一個一致性 Hash 演算法

一個都訪問不到了。這就是普通的取餘的缺點,在增減機器的情況下,這種結果無法接收。

再看看一致性 hash 如何解決。

一致性 Hash 的結果

關鍵的地方來了。

快取節點物件和實際儲存物件不用更改,改的是什麼?

改的是儲存物件的方式和取出物件的方式,也就是不使用對機器進行取餘的演算法。

新的 NodeArray 物件如下:

static class NodeArray {

/** 按照 鍵 排序*/
TreeMap<Integer, Node> nodes = new TreeMap<>();

void addNode(Node node) {
  nodes.put(node.hashCode(), node);
}

void put(Obj obj) {
  int objHashcode = obj.hashCode();
  Node node = nodes.get(objHashcode);
  if (node != null) {
    node.putObj(obj);
    return;
  }

  // 找到比給定 key 大的集合
  SortedMap<Integer, Node> tailMap = nodes.tailMap(objHashcode);
  // 找到最小的節點
  int nodeHashcode = tailMap.isEmpty() ? nodes.firstKey() : tailMap.firstKey();
  nodes.get(nodeHashcode).putObj(obj);

}

Obj get(Obj obj) {
  Node node = nodes.get(obj.hashCode());
  if (node != null) {
    return node.getObj(obj);
  }

  // 找到比給定 key 大的集合
  SortedMap<Integer, Node> tailMap = nodes.tailMap(obj.hashCode());
  // 找到最小的節點
  int nodeHashcode = tailMap.isEmpty() ? nodes.firstKey() : tailMap.firstKey();
  return nodes.get(nodeHashcode).getObj(obj);
}
}
複製程式碼

該類和之前的類的不同之處在於:

  1. 內部沒有使用陣列,而是使用了有序 Map。
  2. put 方法中,物件如果沒有落到快取節點上,就找比他小的節點且離他最近的。這裡我們使用了 TreeMap 的 tailMap 方法,具體 API 可以看文件。
  3. get 方法中,和 put 步驟相同,否則是取不到物件的。

具體尋找節點的方式如圖:

image.png

相同的測試用例,執行結果如下:

image.png

找到了之前所有的節點。解決了普通 hash 的問題。

總結

程式碼比較簡單,主要是通過 JDK 自帶的 TreeMap 實現的尋找臨近節點。當然,我們這裡也只是測試了新增,關於修改還沒有測試,但思路是一樣的。這裡只是做一個拋磚引玉。

同時,我們也沒有實現虛擬節點,感興趣的朋友可以嘗試一下。

good luck!!!!

相關文章