從Hash到一致性Hash原理(深度好文)

要講一致性Hash原理,先從通常性Hash講起,其實Hash的本質就是一個長度可變的數組,那爲何Hash的時間複雜度是O(1),而其餘類型的數據結構查找都是要遍從來,遍歷去,即使是樹,二叉樹,也是要通過幾回比對,才能斷定查找對象的位置,時間複雜度是O(Log(n)),那爲何Hash不用在數組裏面遍歷呢?node

緣由就在於Hash由存儲的對象自己的HashCode以及數組的長度來決定在數組中的位置,這樣一看到這兩個條件就能夠找到對象在數組中的位置而無需去遍歷數組,但算出這個位置(即Hash值)在各個版本中是不一樣的,如HashMap就是各類位操做.通常咱們本身是用HashCode對數組長度取模來算得對象的Hash值.但數組在位置不夠的狀況下會進行擴容,HashMap就是在3/4的時候進行擴容,但不管如何,擴容後,數組長度變化就要進行一次rehash,也就是從新計算每一個對象在數組中的位置,即hash值.咱們能夠來看一下這段代碼,雖然他沒有HashMap那麼複雜,但原理是同樣,只不過計算Hash值的時候只用了最簡單的取模.算法

public class SeparateChainingHashTable<T> {
    private static final int DEFAULT_TABLE_SIZE = 10;
    private List<T>[] theLists;
    private int currentSize;
    //調用帶參構造器
    public SeparateChainingHashTable() {
        this(DEFAULT_TABLE_SIZE);
    }
    //初始化一個數組,並把數組中每個鏈表初始化
    public SeparateChainingHashTable(int size) {
        //初始化一個11位的數組
        theLists = new LinkedList[nextPrime(size)];
        for (int i = 0;i < theLists.length;i++) {
            theLists[i] = new LinkedList<>();
        }
    }
    private int myhash(T x) {
        //取得對象的hash值
        int hashVal = x.hashCode();
        //hash值對數組長度取模
        hashVal %= theLists.length;
        if (hashVal < 0) {
            hashVal += theLists.length;
        }
        return hashVal;
    }
    public void insert(T x) {
        //從鏈表數組中取得第該對象哈希值位的鏈表.
        List<T> whichList = theLists[myhash(x)];
        //若是該鏈表不包含該對象,則鏈表添加該對象
        if (!whichList.contains(x)) {
            whichList.add(x);
            //若是currentSize加1後大於數組的長度,擴容從新計算hash
            if (++currentSize > theLists.length)
                rehash();
        }
    }
    public void remove(T x) {
        List<T> whichList = theLists[myhash(x)];
        if (whichList.contains(x)) {
            whichList.remove(x);
            currentSize--;
        }
    }
    public boolean contains(T x) {
        List<T> whichList = theLists[myhash(x)];
        return whichList.contains(x);
    }
    public void makeEmpty() {
        for (int i = 0;i < theLists.length;i++) {
            theLists[i].clear();
        }
        currentSize = 0;
    }
    private void rehash() {
        List<T>[] oldLists = theLists;
        //進行一次擴容,擴容後長度爲23,可是是一個新的數組
        theLists = new List[nextPrime(2 * theLists.length)];
        for(int j = 0;j < theLists.length;j++){
            //初始化新數組中的每個鏈表
            theLists[j] = new LinkedList<T>();
        }
        //將新數組的currentSize歸0
        currentSize = 0;
        //將原有的鏈表對象放入新數組中,並從新取模計算hash值
        for (int i = 0; i < oldLists.length; i++) {
            for (T item : oldLists[i]) {
                insert(item);
            }
        }
    }
    private static int nextPrime(int num) {
        if (num == 0 || num == 1 || num == 2) {
            return 2;
        }
        if (num % 2 == 0) {
            num++;
        }
        while (!isPrime(num)) {
            num += 2;
        }
        return num;
    }
    private static boolean isPrime(int num) {
        if (num == 2 || num == 3) {
            return true;
        }
        if (num == 1 || num % 2 == 0) {
            return false;
        }
        for (int i = 3; i * i <= num; i += 2) {
            if (num % i == 0) {
                return false;
            }
        }
        return true;
    }
    public void printTable() {
        for(int i = 0;i < theLists.length;i++){
            System.out.println("-----");
            Iterator iterator = theLists[i].iterator();
            while(iterator.hasNext()){
                System.out.print(iterator.next() + " ");
            }
            System.out.println();
        }
    }
    public static void main(String[] args) {
        Random random = new Random();
        SeparateChainingHashTable<Integer> hashTable = new SeparateChainingHashTable<Integer>();
        for (int i = 0; i < 30; i++) {
            Integer tmp = random.nextInt(30);
            hashTable.insert(tmp);
            System.out.printf(tmp + "\t");
        }
        hashTable.printTable();
    }
}

運行結果:數組

0    17    15    20    14    8    7    2    28    12    10    5    25    11    22    13    9    17    20    8    8    14    28    28    24    4    11    26    9    15    -----

-----
24 
-----
2 25 
-----
26 
-----

-----
5 28 
-----服務器

-----

-----

-----

-----
10 
-----
11 
-----
12 
-----
13 
-----
14 
-----
15 
-----數據結構

-----
17 
-----負載均衡

-----dom

-----
20 
-----測試

-----
22 大數據

以上有兩個數排在一塊兒的是由於他們在rehash後有相同的hash值,並被放入鏈表的第一位和第二位.咱們這裏存儲的對象是一個LinkedList的鏈表,而HashMap存儲的是一個Map對象,至於你本身要寫一個Hash要存儲什麼對象那是你本身的事.而他們的擴容方式也是不一樣的,至於如何擴容那也是你本身的事.this

知道了普通Hash的原理,咱們來看看一致性Hash.一致性Hash是由一個固定長度的Hash環構成,大小爲2的32次方.通常用在服務器集羣的增刪節點的處理上,根據節點名稱的Hash值(其分佈爲[0, 232-1])將服務器節點放置在這個Hash環上,而後根據數據的Key值計算獲得其Hash值(其分佈也爲[0, 232-1]),接着在Hash環上順時針查找距離這個Key值的Hash值最近的服務器節點,完成Key到服務器的映射查找。(以上斜體紅色爲次方).這裏咱們有一個問題,就是構建一致性Hash環用什麼數據結構,難道也要用數組?固然不是,咱們要根據咱們的數據Key值進入Hash環的Hash值來查找服務器節點的Hash值的最短期複雜度來決定,這就一樣存在着查找的問題.

首先咱們要對服務器節點的Hash值進行一個存儲,是否要排序,如何查找他們最快,是解決這個問題的關鍵.通常在查找中的時間複雜度以下.

O(1) < O(log2N) < O(n) < O(N * log2N) < O(N2) < O(N3) < 2N < 3N < N!

咱們知道查找最快的是樹,比數組,鏈表都快.因此咱們就選用紅黑樹來創建這個Hash環,而Java中已經有TreeMap和TreeSet都實現了紅黑樹.以TreeMap爲例,TreeMap自己提供了一個tailMap(T fromKey)方法,支持從紅黑樹中查找比fromKey大的值的集合,但並不須要遍歷整個數據結構。使用紅黑樹,可使得查找的時間複雜度爲O(logN).咱們對ArrayList,LinkedList和TreeMap進行比對

能夠看到,數據查找的效率,TreeMap是完勝的,其實再增大數據測試也是同樣的,紅黑樹的數據結構決定了任何一個大於N的最小數據,它都只須要幾回至幾十次查找就能夠查到。查找快,可是插入慢,這是紅黑樹的特色決定的.爲了維護紅黑樹的平衡性,插入效率,紅黑樹在三種數據結構裏是最差的.

定義出來的Hash環以下

private SortedMap<Long, T> circle = new TreeMap();

重寫HashCode的算法

爲何要重寫HashCode算法,由於Java自己自帶的HashCode算法鏈接太緊密.

public class StringHashCodeTest {
    public static void main(String[] args) {
        System.out.println("192.168.0.0:111的哈希值:" + "192.168.0.0:1111".hashCode());
        System.out.println("192.168.0.1:111的哈希值:" + "192.168.0.1:1111".hashCode());
        System.out.println("192.168.0.2:111的哈希值:" + "192.168.0.2:1111".hashCode());
        System.out.println("192.168.0.3:111的哈希值:" + "192.168.0.3:1111".hashCode());
        System.out.println("192.168.0.4:111的哈希值:" + "192.168.0.4:1111".hashCode());
    }
}

運行結果:

192.168.0.0:111的哈希值:1845870087
192.168.0.1:111的哈希值:1874499238
192.168.0.2:111的哈希值:1903128389
192.168.0.3:111的哈希值:1931757540
192.168.0.4:111的哈希值:1960386691

咱們知道咱們的Hash環是2的32次方,而這幾個Hash值分佈在這個環上面,簡直挨的太緊,不利於進入服務器數據的均勻分佈,由於進入服務器數據自己的Hash值可能在他們其間的不多不多,要麼都進最大的的哈希值服務器,要麼都進最小的哈希值服務器.因此咱們要重寫HashCode的計算方式使得服務器的Hash值在Hash環中均勻分佈.如下是重寫的兩個計算Hash值的算法.

/**
 * 使用MD5算法
 * @param key
 * @return
 */
private static long md5HashingAlg(String key) {
    MessageDigest md5 = null;
    try {
        md5 = MessageDigest.getInstance("MD5");
        md5.reset();
        md5.update(key.getBytes());
        byte[] bKey = md5.digest();
        long res = ((long) (bKey[3] & 0xFF) << 24) | ((long) (bKey[2] & 0xFF) << 16) | ((long) (bKey[1] & 0xFF) << 8)| (long) (bKey[0] & 0xFF);
        return res;
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    }
    return 0l;
}

/**
 * 使用FNV1hash算法
 * @param key
 * @return
 */
private static long fnv1HashingAlg(String key) {
    final int p = 16777619;
    int hash = (int) 2166136261L;
    for (int i = 0; i < key.length(); i++)
        hash = (hash ^ key.charAt(i)) * p;
    hash += hash << 13;
    hash ^= hash >> 7;
    hash += hash << 3;
    hash ^= hash >> 17;
    hash += hash << 5;
    return hash;
}

雖然咱們但願重寫HashCode算法後,但願可以在Hash環中均勻分佈服務器節點,但依然有可能分佈不均勻.示例以下

public class ConsistentHashingWithoutVirtualNode {
    private static String[] servers = {"192.168.0.0:111", "192.168.0.1:111", "192.168.0.2:111", "192.168.0.3:111", "192.168.0.4:111"};
    private static SortedMap<Integer, String> sortedMap = new TreeMap<Integer, String>();

    static {
        for (int i = 0; i < servers.length; i++) {
            int hash = getHash(servers[i]);
            System.out.println("[" + servers[i] + "]加入集合中, 其Hash值爲" + hash);
            sortedMap.put(hash, servers[i]);
        }
        System.out.println();
    }

    private static int getHash(String str) {
        final int p = 16777619;
        int hash = (int) 2166136261L;
        for (int i = 0; i < str.length(); i++)
            hash = (hash ^ str.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;
    }

    private static String getServer(String node) {
        int hash = getHash(node);
        Integer i;
        //取得服務器Key大於傳入數據的hash值的全部TreeMap節點
        SortedMap<Integer, String> subMap = sortedMap.tailMap(hash);
        //從新獲得的TreeMap得到第一個Key
        if (subMap.size() == 0) {
            i = sortedMap.firstKey();
        } else {
            i = subMap.firstKey();
        }
        //獲得該Key的服務器IP地址,端口號,即value.
        return subMap.get(i);
    }

    public static void main(String[] args) {
        String[] nodes = {"127.0.0.1:1111", "221.226.0.1:2222", "10.211.0.1:3333"};
        for (int i = 0; i < nodes.length; i++)
            System.out.println("[" + nodes[i] + "]的hash值爲" + getHash(nodes[i]) + ", 被路由到結點[" + getServer(nodes[i]) + "]");
    }
}

運行結果:

[192.168.0.0:111]加入集合中, 其Hash值爲575774686
[192.168.0.1:111]加入集合中, 其Hash值爲8518713
[192.168.0.2:111]加入集合中, 其Hash值爲1361847097
[192.168.0.3:111]加入集合中, 其Hash值爲1171828661
[192.168.0.4:111]加入集合中, 其Hash值爲1764547046

[127.0.0.1:1111]的hash值爲380278925, 被路由到結點[192.168.0.0:111]
[221.226.0.1:2222]的hash值爲1493545632, 被路由到結點[192.168.0.4:111]
[10.211.0.1:3333]的hash值爲1393836017, 被路由到結點[192.168.0.4:111]

咱們只有祭出終極必殺——虛擬節點

很明顯上例中,有5個服務器節點,可是進入服務器集羣的3個數據卻有2個分配到了同一個服務器節點上,這分明就是負載不均.

如今咱們將這些實體服務器節點進行虛擬化,給他們創造分身:虛擬節點.將一個物理節點拆分爲多個虛擬節點,而且同一個物理節點的虛擬節點儘可能均勻分佈在Hash環上。

至於一個物理節點應該拆分爲多少虛擬節點,下面能夠先看一張圖:

橫軸表示須要爲每臺福利服務器擴展的虛擬節點倍數,縱軸表示的是實際物理服務器數。能夠看出,物理服務器不多,須要更大的虛擬節點;反之物理服務器比較多,虛擬節點就能夠少一些。好比有10臺物理服務器,那麼差很少須要爲每臺服務器增長100~200個虛擬節點才能夠達到真正的負載均衡。

public class ConsistentHashingWithVirtualNode {
    private static String[] servers = {"192.168.0.0:111", "192.168.0.1:111", "192.168.0.2:111","192.168.0.3:111", "192.168.0.4:111"};
    //真實節點,真實節點將不保存在Hash環中
    private static List<String> realNodes = new LinkedList<String>();
    //虛擬節點,Hash環
    private static SortedMap<Integer, String> virtualNodes = new TreeMap<Integer, String>();
    //每一個真實節點對應的虛擬節點數
    private static final int VIRTUAL_NODES = 10;
    static {
        //添加真實節點
        for (int i = 0; i < servers.length; i++)
            realNodes.add(servers[i]);
        //添加虛擬節點
        for (String str : realNodes) {
            for (int i = 0; i < VIRTUAL_NODES; i++) {
                //給虛擬節點命名
                String virtualNodeName = str + "&&VN" + String.valueOf(i);
                //重寫Hash算法後的虛擬節點的Hash值
                int hash = getHash(virtualNodeName);
                System.out.println("虛擬節點[" + virtualNodeName + "]被添加, hash值爲" + hash);
                virtualNodes.put(hash, virtualNodeName);
            }
        }
        System.out.println();
    }

    private static int getHash(String str) {
        final int p = 16777619;
        int hash = (int) 2166136261L;
        for (int i = 0; i < str.length(); i++)
            hash = (hash ^ str.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;
    }

    private static String getServer(String node) {
        int hash = getHash(node);
        String virtualNode;
        Integer i;
        SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash);
        if (subMap.size() == 0) {
            i = virtualNodes.firstKey();
            virtualNode = virtualNodes.get(i);
        } else {
            i = subMap.firstKey();
            virtualNode = subMap.get(i);
        }
        //返回真實節點的IP,端口,而不是虛擬節點名稱
        return virtualNode.substring(0, virtualNode.indexOf("&&"));
    }

    public static void main(String[] args) {
        String[] nodes = {"127.0.0.1:1111", "221.226.0.1:2222", "10.211.0.1:3333"};
        for (int i = 0; i < nodes.length; i++)
            System.out.println("[" + nodes[i] + "]的hash值爲" + getHash(nodes[i]) + ", 被路由到結點[" + getServer(nodes[i]) + "]");
    }
}

這樣咱們就能夠獲得密密麻麻的虛擬節點了(這裏注意加入Hash環的只有虛擬節點,沒有真實節點),運行結果以下:

虛擬節點[192.168.0.0:111&&VN0]被添加, hash值爲1686427075
虛擬節點[192.168.0.0:111&&VN1]被添加, hash值爲354859081
虛擬節點[192.168.0.0:111&&VN2]被添加, hash值爲1306497370
虛擬節點[192.168.0.0:111&&VN3]被添加, hash值爲817889914
虛擬節點[192.168.0.0:111&&VN4]被添加, hash值爲396663629
虛擬節點[192.168.0.0:111&&VN5]被添加, hash值爲1220868525
虛擬節點[192.168.0.0:111&&VN6]被添加, hash值爲213398042
虛擬節點[192.168.0.0:111&&VN7]被添加, hash值爲1296671064
虛擬節點[192.168.0.0:111&&VN8]被添加, hash值爲1718596903
虛擬節點[192.168.0.0:111&&VN9]被添加, hash值爲1942098080
虛擬節點[192.168.0.1:111&&VN0]被添加, hash值爲1032739288
虛擬節點[192.168.0.1:111&&VN1]被添加, hash值爲707592309
虛擬節點[192.168.0.1:111&&VN2]被添加, hash值爲302114528
虛擬節點[192.168.0.1:111&&VN3]被添加, hash值爲36526861
虛擬節點[192.168.0.1:111&&VN4]被添加, hash值爲848442551
虛擬節點[192.168.0.1:111&&VN5]被添加, hash值爲779152590
虛擬節點[192.168.0.1:111&&VN6]被添加, hash值爲105241177
虛擬節點[192.168.0.1:111&&VN7]被添加, hash值爲391408881
虛擬節點[192.168.0.1:111&&VN8]被添加, hash值爲1058221668
虛擬節點[192.168.0.1:111&&VN9]被添加, hash值爲48793816
虛擬節點[192.168.0.2:111&&VN0]被添加, hash值爲1452694222
虛擬節點[192.168.0.2:111&&VN1]被添加, hash值爲2023612840
虛擬節點[192.168.0.2:111&&VN2]被添加, hash值爲697907480
虛擬節點[192.168.0.2:111&&VN3]被添加, hash值爲790847074
虛擬節點[192.168.0.2:111&&VN4]被添加, hash值爲2010506136
虛擬節點[192.168.0.2:111&&VN5]被添加, hash值爲866437122
虛擬節點[192.168.0.2:111&&VN6]被添加, hash值爲149660808
虛擬節點[192.168.0.2:111&&VN7]被添加, hash值爲1775912123
虛擬節點[192.168.0.2:111&&VN8]被添加, hash值爲663860070
虛擬節點[192.168.0.2:111&&VN9]被添加, hash值爲1126545273
虛擬節點[192.168.0.3:111&&VN0]被添加, hash值爲891084251
虛擬節點[192.168.0.3:111&&VN1]被添加, hash值爲1725031739
虛擬節點[192.168.0.3:111&&VN2]被添加, hash值爲1127720370
虛擬節點[192.168.0.3:111&&VN3]被添加, hash值爲676720500
虛擬節點[192.168.0.3:111&&VN4]被添加, hash值爲2050578780
虛擬節點[192.168.0.3:111&&VN5]被添加, hash值爲490504949
虛擬節點[192.168.0.3:111&&VN6]被添加, hash值爲2072852996
虛擬節點[192.168.0.3:111&&VN7]被添加, hash值爲1058823147
虛擬節點[192.168.0.3:111&&VN8]被添加, hash值爲2014386380
虛擬節點[192.168.0.3:111&&VN9]被添加, hash值爲1763758471
虛擬節點[192.168.0.4:111&&VN0]被添加, hash值爲586921010
虛擬節點[192.168.0.4:111&&VN1]被添加, hash值爲184078390
虛擬節點[192.168.0.4:111&&VN2]被添加, hash值爲1331645117
虛擬節點[192.168.0.4:111&&VN3]被添加, hash值爲918790803
虛擬節點[192.168.0.4:111&&VN4]被添加, hash值爲1232193678
虛擬節點[192.168.0.4:111&&VN5]被添加, hash值爲1322955826
虛擬節點[192.168.0.4:111&&VN6]被添加, hash值爲922655758
虛擬節點[192.168.0.4:111&&VN7]被添加, hash值爲1658127198
虛擬節點[192.168.0.4:111&&VN8]被添加, hash值爲669639717
虛擬節點[192.168.0.4:111&&VN9]被添加, hash值爲938227397

[127.0.0.1:1111]的hash值爲380278925, 被路由到結點[192.168.0.1:111]
[221.226.0.1:2222]的hash值爲1493545632, 被路由到結點[192.168.0.4:111]
[10.211.0.1:3333]的hash值爲1393836017, 被路由到結點[192.168.0.2:111]

由結果咱們能夠看出,進入服務器集羣的3個數據被分配到了3個不一樣的真實服務器節點上面了.

相關文章
相關標籤/搜索