1. 程式人生 > 程式設計 >美麗的一致性Hash演演算法

美麗的一致性Hash演演算法

如果在大型高併發系統需要資料的分散式儲存 希望資料均勻分佈可擴充套件性強那麼一致性hash演演算法就可以完美解決這個問題 一致性hash演演算法的應用再很多領域 快取 hadoop ES 分散式資料庫

一致性Hash演演算法原理

一致性Hash演演算法是使用取模的方法,一致性的Hash演演算法是對2的32方取模。即,一致性Hash演演算法將整個Hash空間組織成一個虛擬的圓環,Hash函式的值空間為0 ~ 2^32 - 1(一個32位無符號整型),整個雜湊環如下: hash值是個整數 非負,對叢集的某個屬性比如節點名取hash值放到環上,對資料key取hash值也放到環上,按照順時針方向找到離它最近的節點放到它上面, 整個圓環以順時針方向組織,圓環0點右側的第一個點代表n1伺服器,以此類推。 我們將各個伺服器使用Hash進行一個雜湊,具體可以選擇伺服器的IP或主機名作為關鍵字進行雜湊,這樣每臺伺服器就確定在了雜湊環的一個位置上,比如我們有三臺機器,使用IP地址雜湊後在環空間的位置如圖所示:

圖片.png
我們使用以下演演算法定位資料訪問到相應的伺服器:

將資料Key使用相同的函式Hash計算出雜湊值,並確定此資料在環上的位置,從此位置沿環順時針查詢,遇到的伺服器就是其應該定位到的伺服器。

如下圖三個資料O1,O2,O3經過雜湊計算後,在環空間上的位置如下: O1-->n1 O2-->n2 O3-->n3

圖片.png

一致性Hash演演算法的容錯性和可擴充套件性

現在,假設我們的n3宕機了,我們從圖中可以看到,n1、n2不會受到影響,只有O3物件被重新定位到n1。所以我們發現,在一致性Hash演演算法中,如果一臺伺服器不可用,受影響的資料僅僅是此伺服器到其環空間前一臺伺服器之間的資料其他不會受到影響。如圖 所示:

圖片.png

現在我們系統增加了一臺伺服器n4,如圖 所示

圖片.png
從圖中可以看出增加伺服器後資料O2,O3沒有收到影響只有O1受到影響了重新定位到新的節點n4上了。

一致性Hash演演算法對於節點的增減都只需重定位環空間中的一小部分資料,有很好的容錯性和可擴充套件性。 在一致性Hash演演算法服務節點太少的情況下,容易因為節點分佈不均勻面造成資料傾斜(被快取的物件大部分快取在某一臺伺服器上)問題,如圖 :

圖片.png

這時我們發現有大量資料集中在節點A上,而節點B只有少量資料。為瞭解決資料傾斜問題,一致性Hash演演算法引入了虛擬節點機制,即對每一個伺服器節點計算多個雜湊,每個計算結果位置都放置一個此服務節點,稱為虛擬節點。 具體操作可以為伺服器IP或主機名後加入編號來實現,實現如圖 所示: 比如一個n1節點我們虛擬100個虛擬節點,在環上的便是虛擬節點,同理n2,n3也有100個虛擬節點,

圖片.png

伺服器對應多個虛擬節點

當資料過來以後如何判斷放置到哪一個伺服器呢

當資料過來入環以後先找到對應的虛擬節點,再通過虛擬節點找到對應的伺服器,這樣通過增加虛擬節點就可以做到資料的均勻分佈,虛擬節點越多資料越均勻,一般我們一個伺服器放置200個虛擬節點即可

資料定位演演算法不變,只需要增加一步:虛擬節點到實際點的對映。 所以加入虛擬節點之後,即使在服務節點很少的情況下,也能做到資料的均勻分佈。 上面幾種情況都是資料理想的情況下均勻分佈的,其實一致性Hash演演算法存在一個資料傾斜問題

演演算法介面類

public interface IHashService {
    Long hash(String key);
}
複製程式碼

演演算法介面實現類

public class HashService implements IHashService {

    /**
     * MurMurHash演演算法,效能高,碰撞率低
     *
     * @param key String
     * @return Long
     */
    public Long hash(String key) {
        ByteBuffer buf = ByteBuffer.wrap(key.getBytes());
        int seed = 0x1234ABCD;

        ByteOrder byteOrder = buf.order();
        buf.order(ByteOrder.LITTLE_ENDIAN);

        long m = 0xc6a4a7935bd1e995L;
        int r = 47;

        long h = seed ^ (buf.remaining() * m);

        long k;
        while (buf.remaining() >= 8) {
            k = buf.getLong();

            k *= m;
            k ^= k >>> r;
            k *= m;

            h ^= k;
            h *= m;
        }

        if (buf.remaining() > 0) {
            ByteBuffer finish = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
            finish.put(buf).rewind();
            h ^= finish.getLong();
            h *= m;
        }

        h ^= h >>> r;
        h *= m;
        h ^= h >>> r;

        buf.order(byteOrder);
        return h;

    }
}
複製程式碼

模擬機器節點

public class Node<T> {
    private String ip;
    private String name;

    public Node(String ip,String name) {
        this.ip = ip;
        this.name = name;
    }

    public String getIp() {
        return ip;
    }

    public void setIp(String ip) {
        this.ip = ip;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    /**
     * 使用IP當做hash的Key
     *
     * @return String
     */
    @Override
    public String toString() {
        return ip;
    }
}
複製程式碼

一致性Hash操作

public class ConsistentHash<T> {
    // Hash函式介面
    private final IHashService iHashService;
    // 每個機器節點關聯的虛擬節點數量
    private final int          numberOfReplicas;
    // 環形虛擬節點
    private final SortedMap<Long,T> circle = new TreeMap<Long,T>();

    public ConsistentHash(IHashService iHashService,int numberOfReplicas,Collection<T> nodes) {
        this.iHashService = iHashService;
        this.numberOfReplicas = numberOfReplicas;
        for (T node : nodes) {
            add(node);
        }
    }

    /**
     * 增加真實機器節點
     *
     * @param node T
     */
    public void add(T node) {
        for (int i = 0; i < this.numberOfReplicas; i++) {
            circle.put(this.iHashService.hash(node.toString() + i),node);
        }
    }

    /**
     * 刪除真實機器節點
     *
     * @param node T
     */
    public void remove(T node) {
        for (int i = 0; i < this.numberOfReplicas; i++) {
            circle.remove(this.iHashService.hash(node.toString() + i));
        }
    }

    public T get(String key) {
        if (circle.isEmpty()) return null;

        long hash = iHashService.hash(key);

        // 沿環的順時針找到一個虛擬節點
        if (!circle.containsKey(hash)) {
            SortedMap<Long,T> tailMap = circle.tailMap(hash);
            hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
        }
        return circle.get(hash);
    }
}
複製程式碼

測試類


public class TestHashCircle {
    // 機器節點IP字首
    private static final String IP_PREFIX = "192.168.0.";

    public static void main(String[] args) {
        // 每臺真實機器節點上儲存的記錄條數
        Map<String,Integer> map = new HashMap<String,Integer>();

        // 真實機器節點,模擬10臺
        List<Node<String>> nodes = new ArrayList<Node<String>>();
        for (int i = 1; i <= 10; i++) {
            map.put(IP_PREFIX + i,0); // 初始化記錄
            Node<String> node = new Node<String>(IP_PREFIX + i,"node" + i);
            nodes.add(node);
        }

        IHashService iHashService = new HashService();
        // 每臺真實機器引入100個虛擬節點
        ConsistentHash<Node<String>> consistentHash = new ConsistentHash<Node<String>>(iHashService,500,nodes);

        // 將5000條記錄儘可能均勻的儲存到10臺機器節點上
        for (int i = 0; i < 5000; i++) {
            // 產生隨機一個字串當做一條記錄,可以是其它更復雜的業務物件,比如隨機字串相當於物件的業務唯一標識
            String data = UUID.randomUUID().toString() + i;
            // 通過記錄找到真實機器節點
            Node<String> node = consistentHash.get(data);
            // 再這裡可以能過其它工具將記錄儲存真實機器節點上,比如MemoryCache等
            // ...
            // 每臺真實機器節點上儲存的記錄條數加1
            map.put(node.getIp(),map.get(node.getIp()) + 1);
        }

        // 列印每臺真實機器節點儲存的記錄條數
        for (int i = 1; i <= 10; i++) {
            System.out.println(IP_PREFIX + i + "節點記錄條數:" + map.get(IP_PREFIX + i));
        }
    }
}

 
複製程式碼

執行結果如下:

圖片.png