redis 一致性hash算法

一、一般算法:

對對象先hash然后對redis數(shù)量取模,如果結(jié)果是0就存在0的節(jié)點上。

1、2同上,假設(shè)有0-3四個redis節(jié)點、20個數(shù)據(jù):

進行取模后分布如下:

image

現(xiàn)在因為壓力過大需要擴容,增加一臺redis4、第五個節(jié)點:

image

現(xiàn)在只有4個節(jié)點還能夠命中。命中率是:4/20 = 20%,命中率極其低下。(redis肯定是不會這樣用的)

二、redis使用的consistent hashing(一致性hash算法)

1、環(huán)形hash空間:

image

把對象映射到0-2^32-1的空間里。

2、把數(shù)據(jù)通過一定的hash算法處理后映射到環(huán)上

現(xiàn)在假設(shè)有4個對象:object1-object4,將四個對象hash后映射到環(huán)形空間中:

image

3、把chche映射到hash空間

(基本思想就是講對象和cache都映射到同一hash數(shù)值空間中,并且使用相同的hash算法,可以使用cache的ip地址或者其他因子),假設(shè)現(xiàn)在有三個cache:

image

4、機器的刪除與添加

每個key順時針往下走,找到的第一個cache節(jié)點就是存儲位置:

image

現(xiàn)在移除一個cacheB節(jié)點、這時候key4將找不到cache,key4繼續(xù)使用一致性hash算法運算后算出最新的cacheC,以后存儲與讀取都將在cacheC上:

image

移除節(jié)點后的影響范圍在該節(jié)點逆時針計算到遇到的第一個cache節(jié)點之間的數(shù)據(jù)節(jié)點。

現(xiàn)在看一下增加一個節(jié)點:

image

影響范圍為:添加節(jié)點逆時針遇到的第一個cache節(jié)點之間的數(shù)據(jù)節(jié)點。

5、平衡性

根據(jù)上面的圖解分析,一致性哈希算法滿足了單調(diào)性和負載均衡的特性以及一般hash算法的分散性,但這還并不能當做其被廣泛應(yīng)用的原由,因為還缺少了平衡性。下面將分析一致性哈希算法是如何滿足平衡性的。

hash算法是不保證平衡的,如上面只部署了NODE1和NODE3的情況(NODE2被刪除的圖),object1存儲到了NODE1中,而object2、object3、object4都存儲到了NODE3中,這樣NODE3節(jié)點由于承擔了NODE2節(jié)點的數(shù)據(jù),所以NODE3節(jié)點的負載會變高,NODE3節(jié)點很容易也宕機,這樣依次下去可能造成整個集群都掛了。
在一致性哈希算法中,為了盡可能的滿足平衡性,其引入了虛擬節(jié)點?!疤摂M節(jié)點”( virtual node )是實際節(jié)點(機器)在 hash 空間的復(fù)制品(replica),一實際個節(jié)點(機器)對應(yīng)了若干個“虛擬節(jié)點”,這個對應(yīng)個數(shù)也成為“復(fù)制個數(shù)”,“虛擬節(jié)點”在 hash 空間中以hash值排列。即把想象在這個環(huán)上有很多“虛擬節(jié)點”,數(shù)據(jù)的存儲是沿著環(huán)的順時針方向找一個虛擬節(jié)點,每個虛擬節(jié)點都會關(guān)聯(lián)到一個真實節(jié)點。
圖中的A1、A2、B1、B2、C1、C2、D1、D2都是虛擬節(jié)點,機器A負載存儲A1、A2的數(shù)據(jù),機器B負載存儲B1、B2的數(shù)據(jù),機器C負載存儲C1、C2的數(shù)據(jù)。由于這些虛擬節(jié)點數(shù)量很多,均勻分布,因此不會造成“雪崩”現(xiàn)象。


使用虛擬節(jié)點的思想,為每個物理節(jié)點(服務(wù)器)在圓上分配100~200個點。這樣就能抑制分布不均勻,最大限度地減小服務(wù)器增減時的緩存重新分布。用戶數(shù)據(jù)映射在虛擬節(jié)點上,就表示用戶數(shù)據(jù)真正存儲位置是在該虛擬節(jié)點代表的實際物理服務(wù)器上。
下面有一個圖描述了需要為每臺物理服務(wù)器增加的虛擬節(jié)點。


image

x軸表示的是需要為每臺物理服務(wù)器擴展的虛擬節(jié)點倍數(shù)(scale),y軸是實際物理服務(wù)器數(shù),可以看出,當物理服務(wù)器的數(shù)量很小時,需要更大的虛擬節(jié)點,反之則需要更少的節(jié)點,從圖上可以看出,在物理服務(wù)器有10臺時,差不多需要為每臺服務(wù)器增加100~200個虛擬節(jié)點才能達到真正的負載均衡。

簡單的java代碼實現(xiàn):

class ConsistentHash<T> {
 
    /**
     * 哈希函數(shù)
     */
    private final HashFunction hashFunction;
 
    /**
     * 虛擬節(jié)點數(shù) , 越大分布越均衡,但越大,在初始化和變更的時候效率差一點。 測試中,設(shè)置200基本就均衡了。
     */
    private final int numberOfReplicas;
 
    /**
     * 環(huán)形Hash空間
     */
    private final SortedMap<Integer, T> circle = new TreeMap<Integer, T>();
 
    /**
     * @param hashFunction
     *            ,哈希函數(shù)
     * @param numberOfReplicas
     *            ,虛擬服務(wù)器系數(shù)
     * @param nodes
     *            ,服務(wù)器節(jié)點
     */
    public ConsistentHash(HashFunction hashFunction, int numberOfReplicas,
            Collection<T> nodes) {
        this.hashFunction = hashFunction;
        this.numberOfReplicas = numberOfReplicas;
 
        for (T node : nodes) {
            this.addNode(node);
        }
    }
 
    /**
     * 添加物理節(jié)點,每個node 會產(chǎn)生numberOfReplicas個虛擬節(jié)點,這些虛擬節(jié)點對應(yīng)的實際節(jié)點是node
     */
    public void addNode(T node) {
        for (int i = 0; i < numberOfReplicas; i++) {
            int hashValue = hashFunction.hash(node.toString() + i);
            circle.put(hashValue, node);
        }
    }
 
    /**移除物理節(jié)點,將node產(chǎn)生的numberOfReplicas個虛擬節(jié)點全部移除
     * @param node
     */
    public void removeNode(T node) {
        for (int i = 0; i < numberOfReplicas; i++) {
            int hashValue = hashFunction.hash(node.toString() + i);
            circle.remove(hashValue);
        }
    }
 
    /**
     * 得到映射的物理節(jié)點
     * 
     * @param key
     * @return
     */
    public T getNode(Object key) {
        if (circle.isEmpty()) {
            return null;
        }
        int hashValue = hashFunction.hash(key);
//      System.out.println("key---" + key + " : hash---" + hash);
        if (!circle.containsKey(hashValue)) {
            // 返回鍵大于或等于hash的node,即沿環(huán)的順時針找到一個虛擬節(jié)點
            SortedMap<Integer, T> tailMap = circle.tailMap(hashValue);
            // System.out.println(tailMap);
            // System.out.println(circle.firstKey());
            hashValue = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
        }
//      System.out.println("hash---: " + hash);
        return circle.get(hashValue);
    }
 
    static class HashFunction {
        /**
         * MurMurHash算法,是非加密HASH算法,性能很高,
         * 比傳統(tǒng)的CRC32,MD5,SHA-1(這兩個算法都是加密HASH算法,復(fù)雜度本身就很高,帶來的性能上的損害也不可避免)
         * 等HASH算法要快很多,而且據(jù)說這個算法的碰撞率很低. http://murmurhash.googlepages.com/
         */
        int hash(Object key) {
            ByteBuffer buf = ByteBuffer.wrap(key.toString().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 (int) h;
        }
    }
}
public class Test {
 
    public static void main(String[] args) {
        HashSet<String> serverNode = new HashSet<String>();
        serverNode.add("127.1.1.1#A");
        serverNode.add("127.2.2.2#B");
        serverNode.add("127.3.3.3#C");
        serverNode.add("127.4.4.4#D");
 
        Map<String, Integer> serverNodeMap = new HashMap<String, Integer>();
 
        ConsistentHash<String> consistentHash = new ConsistentHash<String>(
                new HashFunction(), 200, serverNode);
 
        int count = 50000;
 
        for (int i = 0; i < count; i++) {
            String serverNodeName = consistentHash.getNode(i);
            // System.out.println(i + " 映射到物理節(jié)點---" + serverNodeName);
            if (serverNodeMap.containsKey(serverNodeName)) {
                serverNodeMap.put(serverNodeName,
                        serverNodeMap.get(serverNodeName) + 1);
            } else {
                serverNodeMap.put(serverNodeName, 1);
            }
        }
        // System.out.println(serverNodeMap);
 
        showServer(serverNodeMap);
        serverNodeMap.clear();
 
        consistentHash.removeNode("127.1.1.1#A");
        System.out.println("-------------------- remove 127.1.1.1#A");
 
        for (int i = 0; i < count; i++) {
            String serverNodeName = consistentHash.getNode(i);
            // System.out.println(i + " 映射到物理節(jié)點---" + serverNodeName);
            if (serverNodeMap.containsKey(serverNodeName)) {
                serverNodeMap.put(serverNodeName,
                        serverNodeMap.get(serverNodeName) + 1);
            } else {
                serverNodeMap.put(serverNodeName, 1);
            }
        }
 
        showServer(serverNodeMap);
        serverNodeMap.clear();
 
        consistentHash.addNode("127.5.5.5#E");
        System.out.println("-------------------- add 127.5.5.5#E");
 
        for (int i = 0; i < count; i++) {
            String serverNodeName = consistentHash.getNode(i);
            // System.out.println(i + " 映射到物理節(jié)點---" + serverNodeName);
            if (serverNodeMap.containsKey(serverNodeName)) {
                serverNodeMap.put(serverNodeName,
                        serverNodeMap.get(serverNodeName) + 1);
            } else {
                serverNodeMap.put(serverNodeName, 1);
            }
        }
 
        showServer(serverNodeMap);
        serverNodeMap.clear();
 
        consistentHash.addNode("127.6.6.6#F");
        System.out.println("-------------------- add 127.6.6.6#F");
        count *= 2;
        System.out.println("-------------------- 業(yè)務(wù)量加倍");
        for (int i = 0; i < count; i++) {
            String serverNodeName = consistentHash.getNode(i);
            // System.out.println(i + " 映射到物理節(jié)點---" + serverNodeName);
            if (serverNodeMap.containsKey(serverNodeName)) {
                serverNodeMap.put(serverNodeName,
                        serverNodeMap.get(serverNodeName) + 1);
            } else {
                serverNodeMap.put(serverNodeName, 1);
            }
        }
        showServer(serverNodeMap);
 
    }
 
    /**
     * 服務(wù)器運行狀態(tài)
     * 
     * @param map
     */
    public static void showServer(Map<String, Integer> map) {
        for (Entry<String, Integer> m : map.entrySet()) {
            System.out.println(m.getKey() + ", 存儲數(shù)據(jù)量 " + m.getValue());
        }
    }

運行結(jié)果:

127.4.4.4#D, 存儲數(shù)據(jù)量 13177
127.2.2.2#B, 存儲數(shù)據(jù)量 11834
127.3.3.3#C, 存儲數(shù)據(jù)量 12827
127.1.1.1#A, 存儲數(shù)據(jù)量 12162
-------------------- remove 127.1.1.1#A
127.4.4.4#D, 存儲數(shù)據(jù)量 17696
127.2.2.2#B, 存儲數(shù)據(jù)量 15114
127.3.3.3#C, 存儲數(shù)據(jù)量 17190
-------------------- add 127.5.5.5#E
127.4.4.4#D, 存儲數(shù)據(jù)量 12154
127.2.2.2#B, 存儲數(shù)據(jù)量 11878
127.3.3.3#C, 存儲數(shù)據(jù)量 12908
127.5.5.5#E, 存儲數(shù)據(jù)量 13060
-------------------- add 127.6.6.6#F
-------------------- 業(yè)務(wù)量加倍
127.4.4.4#D, 存儲數(shù)據(jù)量 18420
127.2.2.2#B, 存儲數(shù)據(jù)量 20197
127.6.6.6#F, 存儲數(shù)據(jù)量 21015
127.5.5.5#E, 存儲數(shù)據(jù)量 19038
127.3.3.3#C, 存儲數(shù)據(jù)量 21330

6、缺陷


一致性hash存在數(shù)據(jù)不一致問題:分析如下步驟

  • NODE2 存儲Key2 = 100
  • 此時NODE2掛了,key2 = 200 會落到object2內(nèi)
  • NODE2再次恢復(fù),但NODE2 key2 = 100,而object2 key2 = 200,數(shù)據(jù)不一致。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容