1. 程式人生 > >java簡單實現一致性雜湊演算法

java簡單實現一致性雜湊演算法

什麼是一致性雜湊演算法
一種特殊的雜湊演算法,這種演算法使得雜湊表、叢集的規模在伸縮時儘可能減少重對映(remap)。

為什麼需要它
一致性雜湊基本解決了在P2P環境中最為關鍵的問題——如何在動態的網路拓撲(叢集)中分佈儲存和路由。每個節點僅需維護少量相鄰節點的資訊,並且在節點加入/退出系統時,僅有相關的少量節點參與到拓撲的維護中。

兩種常見的一致性雜湊演算法
餘數hash
hash_ip(請求者的ip的hashCode) % slot_Num(節點數) = n,n為節點的序號假設現在有3臺快取伺服器,現在有20個ip來訪問快取叢集,假設3個節點的序號為0~2,20個ip的hashCode為0~19,那麼路由情況如下:如果擴充套件到4臺伺服器,那麼只有標紅色能命中快取,並且隨著伺服器的增加,快取的命中率遞減
hash_ip 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
路由到的節點 0 1 2 0 1 2 0 1 2 0 1 2 0 1 2 0 1 2 0 1
紅色的為命中的快取,黑色的快取都shixiao
hash_ip 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
路由到的節點 0 1 2 3 0 1 2 3 0 1 2 3 0 1 2 3 0 1 2 3
從上面的例子中可以看出餘數hash的伸縮性不好,當我們進行擴容時,多數快取失效,壓力全部移到資料庫上,有可能導致資料庫宕機。
這個問題的解決方案:
1.在網站訪問量低的時候,比如凌晨,進行擴容
2.傳送模擬請求進行快取預熱,使資料在快取叢集上重新分佈

一致性hash演算法

原理:先構造一個長度為232的整數環(這個環被稱為一致性Hash環),根據節點名稱的Hash值(其分佈為[0, 2的32次方-1])將伺服器節點放置在這個Hash環上,然後根據資料的Key值計算得到其Hash值(其分佈也為[0, 2的32次方-1]),接著在Hash環上順時針查詢距離這個Key值的Hash值最近的伺服器節點,完成Key到伺服器的對映查詢。
下圖的大環表示hash環,藍色點表示hashCode[node_ip]在環上的分佈,小點表示hashCode[client_ip],順時針尋找第一個大於hashCode[client_ip]的節點雜湊值,即路由到該節點,由該節點處理請求。這種情況基本解決了伸縮性差的問題,我們隨時可以新增刪除節點到雜湊環上。

但是節點的擴容又導致了一個問題:負載不均。如下圖
當我們增加了node4節點時,那些本來會轉發給node3的請求會被路由到node4上,導致node3處於空閒狀態,而node4壓力較大,這時,虛擬節點出現了,此時雜湊環上的節點都是虛擬節點,多個虛擬節點對映到一個物理節點,也就是說,當我們新增一個節點時,我們不再把物理節點作為一個節點存到雜湊環上,而是用多個虛擬節點來替代一個物理節點,路由到某個虛擬節點後再對映到物理節點。而這多個虛擬節點是分散的,很大程度可以彌補負載不均的缺點

下圖的黃色節點為新新增的虛擬節點,這3個節點實際上值對應一個物理節點,比如物理節點:10.0.0.101:1111,自定義虛擬節點(我在物理節點後加virtual node+序號):10.0.0.101:1111vn1、10.0.0.101:1111vn2、10.0.0.101:1111vn3,這樣對映到雜湊環上分散的,不會像一個節點那樣導致擴容後負載不均,只要虛實節點的比例合理。

從網上找的,物理節點和虛擬節點的比例圖,縱座標表示物理節點數,橫座標表示虛擬節點數,比如物理節點只有10個時,需要100~200個虛擬節點

附帶虛擬節點的一致性hash的java實現
1.使用FNV_1雜湊演算法來雜湊ip(java的hashCode值太集中,不適合)
2.用treeMap來儲存節點的hash值,使用tail方法獲取比hashCode[client_ip]大的子集,取子集的第一個節點
就是我們要的節點。

3.簡單起見,一個物理節點對應4個虛擬節點

package consistence_hash_algorithm;

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

/**
 * writer: holien
 * Time: 2017-10-20 21:07
 * Intent: 使用虛擬節點的一致性雜湊演算法
 */
public class ConsistentHashingWithVirtualNode {
    // 4個物理節點
    private static String[] servers = {"168.10.10.101:6386", "168.10.10.101:6387",
            "168.10.10.101:6388", "168.10.10.101:6389"};
    // 使用SortedMap可以排序,再使用tailMap獲取key比hashCode[client_ip]大的子map
    private static SortedMap virtualNodes;
    private static final int VIRTUAL_NODE_NUM = 4;

    // 模擬線上的4臺伺服器對應的16個虛擬節點
    static {
        virtualNodes = new TreeMap<Integer, String>();
        for (int i = 0, len = servers.length ; i < len; i++) {
            int hashCode;
            String vitualNode;
            for (int j = 0; j < VIRTUAL_NODE_NUM; j++) {
                // 計算節點的雜湊值作為key,節點ip("168.10.10.101:6386vni")作為value
                vitualNode = servers[i] + "vn" + j;
                // 計算虛擬節點的hashCode
                hashCode = getHashCode(vitualNode);
                virtualNodes.putIfAbsent(hashCode, vitualNode);
                System.out.println("添加了虛擬節點:" + vitualNode + ", hashCode:" + hashCode);
            }
        }
    }

    // 使用32位FNV_1計算節點的hashCode
    private static int getHashCode(String node) {
        final int p = 16777619;
        int hash = (int)2166136261L;
        for (int i = 0; i < node.length(); i++)
            hash = (hash ^ node.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;
    }

    // 通過client_ip的雜湊值路由一個虛擬節點,再對映到物理節點
    public static String routeServer(String node) {
        int hashCode = getHashCode(node);
        SortedMap subMap = virtualNodes.tailMap(hashCode);
        int firstKey = (Integer)subMap.firstKey();
        String virtualNode = (String)subMap.get(firstKey);
        // 模擬尋找物理節點,把vni去掉
        String actualNode = virtualNode.substring(0, virtualNode.length() - 3);
        System.out.println(node + "的hashCode為" + hashCode + ",被路由到虛擬節點:"
                + virtualNode + ",對映到物理節點:" + actualNode);
        return actualNode;
    }

    public static void main(String[] args) {
        String[] nodes = {"10.112.11.252:1111", "221.226.0.1:2222", "10.211.0.1:3333"};
        for (int i = 0, len = nodes.length; i < len; i++) {
            routeServer(nodes[i]);
        }
    }

}

執行結果: