美麗的一致性Hash算法

若是在大型高併發系統須要數據的分佈式存儲 但願數據均勻分佈可擴展性強那麼一致性hash算法就能夠完美解決這個問題 一致性hash算法的應用再不少領域 緩存 hadoop ES 分佈式數據庫node

一致性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宕機了,咱們從圖中能夠看到,n一、n2不會受到影響,只有O3對象被從新定位到n1。因此咱們發現,在一致性Hash算法中,若是一臺服務器不可用,受影響的數據僅僅是此服務器到其環空間前一臺服務器之間的數據其餘不會受到影響。如圖 所示: bash

圖片.png

如今咱們系統增長了一臺服務器n4,如圖 所示服務器

圖片.png
從圖中能夠看出增長服務器後數據O2,O3沒有收到影響只有O1受到影響了從新定位到新的節點n4上了。

一致性Hash算法對於節點的增減都只需重定位環空間中的一小部分數據,有很好的容錯性和可擴展性。 在一致性Hash算法服務節點太少的狀況下,容易由於節點分佈不均勻面形成數據傾斜(被緩存的對象大部分緩存在某一臺服務器上)問題,如圖 :併發

圖片.png

這時咱們發現有大量數據集中在節點A上,而節點B只有少許數據。爲了解決數據傾斜問題,一致性Hash算法引入了虛擬節點機制,即對每個服務器節點計算多個哈希,每一個計算結果位置都放置一個此服務節點,稱爲虛擬節點。 具體操做能夠爲服務器IP或主機名後加入編號來實現,實現如圖 所示: 好比一個n1節點咱們虛擬100個虛擬節點,在環上的即是虛擬節點,同理n2,n3也有100個虛擬節點,dom

圖片.png

服務器對應多個虛擬節點

當數據過來之後如何判斷放置到哪個服務器呢

當數據過來入環之後先找到對應的虛擬節點,再經過虛擬節點找到對應的服務器,這樣經過增長虛擬節點就能夠作到數據的均勻分佈,虛擬節點越多數據越均勻,通常咱們一個服務器放置200個虛擬節點便可分佈式

數據定位算法不變,只須要增長一步:虛擬節點到實際點的映射。 因此加入虛擬節點以後,即便在服務節點不多的狀況下,也能作到數據的均勻分佈。 上面幾種狀況都是數據理想的狀況下均勻分佈的,其實一致性Hash算法存在一個數據傾斜問題ide

算法接口類

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
相關文章
相關標籤/搜索