一致性哈希算法及Java實現(xiàn)

1.為什么需要一致性哈希?
在分布式服務集群中如MemCache(一個內(nèi)存中存在的Hashmap),需要提供存儲元素object的路由算法,來計算其應該所在的服務器位置。假設服務器集群是一個數(shù)組int[n-1] (n為服務器個數(shù)) ,如果使用這樣的hash算法:
路由到的服務器的數(shù)組位置:index = hash(object) / n;
當增加一個節(jié)點或者減少一個節(jié)點時生棍,會導致大量元素路由的服務器位置改變,導致請求object落空媳谁。
2.一致性哈希算法
一致性哈希的基本原理就是在一個hash環(huán)上(如范圍0-2^32-1)計算服務器節(jié)點的hash值涂滴,如果一個object要尋找應該路由的服務器節(jié)點,則計算其hash值晴音,并在環(huán)上順時針查找離它最近的節(jié)點柔纵。如圖:


一致性hash規(guī)則.png

如果刪除節(jié)點Cache B,則影響的是CacheA-CacheB之間的key,將會路由至 Cache C ,如object4.


刪除一個節(jié)點.png

如果增加一個節(jié)點Cache D,則原來一部分路由到Cache C的節(jié)點將會路由到Cache D,如object2
增加一個節(jié)點.png

可以看出在節(jié)點發(fā)生變化時一致性哈希相對傳統(tǒng)的哈希取拇冈辏可以減少object重新路由的概率搁料,但是上述哈希分配仍然存在各個節(jié)點所分配的object不均勻的問題。
3.虛擬節(jié)點
如果讓一個物理節(jié)點在hash環(huán)上代表盡可能多的分散均勻的hash值系羞,那么object在各個物理節(jié)點的分配將會更加均勻郭计,如圖:
虛擬節(jié)點.png

虛擬節(jié)點與物理節(jié)點的對應關系如下:
虛擬節(jié)點的關系映射.png

下圖橫軸表示需要為每臺福利服務器擴展的虛擬節(jié)點個數(shù),縱軸表示的是實際物理服務器個數(shù)數(shù)椒振≌焉欤可以看出,物理服務器很少澎迎,需要更大的虛擬節(jié)點勋乾;反之物理服務器比較多宋下,虛擬節(jié)點就可以少一些嗡善。比如有10臺物理服務器辑莫,那么差不多需要為每臺服務器增加100~200個虛擬節(jié)點才可以達到真正的負載均衡。



4.代碼實現(xiàn)
存在兩個問題罩引,一是選取怎樣的hash算法才能夠使得數(shù)據(jù)分布均勻各吨,二是如何快速查找距離最近的服務器節(jié)點是哪個?
(1)String重寫的hashCode()方法在一致性Hash算法上的分布不好袁铐,KETAMA_HASH是默認的MemCache推薦的一致性Hash算法揭蜒,而FNV1_32_HASH算法的效率就會高一些。
(2)這是一個排序問題剔桨,采用紅黑樹時間復雜度為O(LogN),Java中有對應的實現(xiàn)TreeMap,并且TreeMap本身提供了一個tailMap(K fromKey)方法屉更,支持從紅黑樹中查找比fromKey大的值的集合,但并不需要遍歷整個數(shù)據(jù)結(jié)構(gòu)洒缀。

有虛擬節(jié)點的版本實現(xiàn):

public interface HashFunction {
    //hash函數(shù)
    Integer hash(String key);
}

public class HashFunctionImpl implements HashFunction {
    //FNV1_32_HASH算法
    @Override
    public Integer hash(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;

        // 如果算出來的值為負數(shù)則取其絕對值
        if (hash < 0)
            hash = Math.abs(hash);
        return hash;
    }
}

// 物理機節(jié)點模擬類瑰谜,保存節(jié)點的IP、名稱树绩、端口等信息
public class Node {

    private String ip;// 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;
    }
}


public class ConsistentHash {

    private final HashFunction hashFunction;// hash 函數(shù)接口
    private final int numberOfReplicas;// 每個機器節(jié)點關聯(lián)的虛擬節(jié)點個數(shù)
    private final SortedMap<Integer,Node> circle = new TreeMap<>();// 環(huán)形虛擬節(jié)點

    /**
     *
     * @param hashFunction
     *            hash 函數(shù)接口
     * @param numberOfReplicas
     *            每個機器節(jié)點關聯(lián)的虛擬節(jié)點個數(shù)
     * @param nodes
     *            真實機器節(jié)點
     */
    public ConsistentHash(HashFunction hashFunction, int numberOfReplicas, Collection<Node> nodes) {
        this.hashFunction = hashFunction;
        this.numberOfReplicas = numberOfReplicas;

        for (Node node : nodes) {
            add(node);
        }
    }

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

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

    /**
     * 取得真實機器節(jié)點
     *
     * @param key
     * @return
     */
    public Node get(String key) {
        if (circle.isEmpty()) {
            return null;
        }

        Integer hash = hashFunction.hash(key);
        if (!circle.containsKey(hash)) {
            SortedMap<Integer,Node> tailMap = circle.tailMap(hash);// 沿環(huán)的順時針找到一個虛擬節(jié)點
            hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
        }

        return circle.get(hash); // 返回該虛擬節(jié)點對應的真實機器節(jié)點的信息
    }
}

public class ConHashTest {
    private static final String IP_PREFIX = "192.168.1.";// 機器節(jié)點IP前綴
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<String, Integer>();// 每臺真實機器節(jié)點上保存的記錄條數(shù)
        HashFunction hashFunction = new HashFunctionImpl();
        //真實物理節(jié)點
        List<Node> realNodes = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            map.put(IP_PREFIX + i, 0);// 每臺真實機器節(jié)點上保存的記錄條數(shù)初始為0

            Node node = new Node(IP_PREFIX + i, "node" + i);
            realNodes.add(node);
        }


        ConsistentHash consistentHash = new ConsistentHash(hashFunction,100,realNodes);
        // 將10000條記錄盡可能均勻的存儲到10臺機器節(jié)點
        for (int i = 0; i < 10000; i++) {
            // 產(chǎn)生隨機一個字符串當做一條記錄萨脑,可以是其它更復雜的業(yè)務對象,比如隨機字符串相當于對象的業(yè)務唯一標識
            String data = UUID.randomUUID().toString() + i;
            // 通過記錄找到真實機器節(jié)點
            Node node = consistentHash.get(data);
            // 這里可以通過其它工具將記錄存儲真實機器節(jié)點上,比如MemoryCache等
            // ...
            // 每臺真實機器節(jié)點上保存的記錄條數(shù)加1
            map.put(node.getIp(), map.get(node.getIp()) + 1);
        }

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

}

參考資料:
https://cloud.tencent.com/developer/article/1084801
https://www.codeproject.com/Articles/56138/Consistent-hashing
http://sundoctor.iteye.com/blog/2104321
一致性哈希論文

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末饺饭,一起剝皮案震驚了整個濱河市渤早,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瘫俊,老刑警劉巖鹊杖,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異扛芽,居然都是意外死亡骂蓖,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門胸哥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來涯竟,“玉大人,你說我怎么就攤上這事空厌÷” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵嘲更,是天一觀的道長筐钟。 經(jīng)常有香客問我,道長赋朦,這世上最難降的妖魔是什么篓冲? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任李破,我火速辦了婚禮,結(jié)果婚禮上壹将,老公的妹妹穿的比我還像新娘嗤攻。我一直安慰自己,他們只是感情好诽俯,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布妇菱。 她就那樣靜靜地躺著,像睡著了一般暴区。 火紅的嫁衣襯著肌膚如雪闯团。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天仙粱,我揣著相機與錄音房交,去河邊找鬼。 笑死伐割,一個胖子當著我的面吹牛候味,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播口猜,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼负溪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了济炎?” 一聲冷哼從身側(cè)響起川抡,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎须尚,沒想到半個月后崖堤,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡耐床,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年密幔,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撩轰。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡胯甩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出堪嫂,到底是詐尸還是另有隱情偎箫,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布皆串,位于F島的核電站淹办,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏恶复。R本人自食惡果不足惜怜森,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一速挑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧副硅,春花似錦姥宝、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至流纹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間违诗,已是汗流浹背漱凝。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留诸迟,地道東北人茸炒。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像阵苇,于是被迫代替她去往敵國和親壁公。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容