一致性Hash原理與實(shí)現(xiàn)

前言

互聯(lián)網(wǎng)公司中,絕大部分都沒有馬爸爸系列的公司那樣財(cái)大氣粗蛋济,他們即沒有強(qiáng)勁的服務(wù)器碗旅、也沒有錢去購買昂貴的海量數(shù)據(jù)庫祟辟。那他們是怎么應(yīng)對(duì)大數(shù)據(jù)量高并發(fā)的業(yè)務(wù)場(chǎng)景的呢?
這個(gè)和當(dāng)前的開源技術(shù)醇份、海量數(shù)據(jù)架構(gòu)都有著不可分割的關(guān)系僚纷。比如通過mysql拗盒、nginx等開源軟件锣咒,通過架構(gòu)和低成本的服務(wù)器搭建千萬級(jí)別的用戶訪問系統(tǒng)毅整。
怎么樣搭建一個(gè)好的系統(tǒng)架構(gòu),這個(gè)話題我們能聊上個(gè)七天七夜艇潭。這里我主要結(jié)合Redis集群來講一下一致性Hash的相關(guān)問題蹋凝。

Redis集群的使用

我們?cè)谑褂肦edis的過程中,為了保證Redis的高可用改含,我們一般會(huì)對(duì)Redis做主從復(fù)制捍壤,組成Master-Master或者Master-Slave的形式鞍爱,進(jìn)行數(shù)據(jù)的讀寫分離睹逃,如下圖1-1所示:

圖1-1:Master-Slave模式

當(dāng)緩存數(shù)據(jù)量超過一定的數(shù)量時(shí)沉填,我們就要對(duì)Redis集群做分庫分表的操作拜轨。

來個(gè)栗子允青,我們有一個(gè)電商平臺(tái)颠锉,需要使用Redis存儲(chǔ)商品的圖片資源琼掠,存儲(chǔ)的格式為鍵值對(duì),key值為圖片名稱悼瓮,Value為該圖片所在的文件服務(wù)器的路徑横堡,我們需要根據(jù)文件名冠桃,查找到文件所在的文件服務(wù)器上的路徑,我們的圖片數(shù)量大概在3000w左右污茵,按照我們的規(guī)則進(jìn)行分庫葬项,規(guī)則就是隨機(jī)分配的民珍,我們以每臺(tái)服務(wù)器存500w的數(shù)量穷缤,部署12臺(tái)緩存服務(wù)器津肛,并且進(jìn)行主從復(fù)制,架構(gòu)圖如下圖1-2所示:
圖1-2:Redis分庫分表

由于我們定義的規(guī)則是隨機(jī)的秸脱,所以我們的數(shù)據(jù)有可能存儲(chǔ)在任何一組Redis中摊唇,比如我們需要查詢"product.png"的圖片巷查,由于規(guī)則的隨機(jī)性岛请,我們需要遍歷所有Redis服務(wù)器警绩,才能查詢得到肩祥。這樣的結(jié)果顯然不是我們所需要的。所以我們會(huì)想到按某一個(gè)字段值進(jìn)行Hash值岸霹、取模松申。所以我們就看看使用Hash的方式是怎么進(jìn)行的贸桶。

使用Hash的Redis集群

如果我們使用Hash的方式皇筛,每一張圖片在進(jìn)行分庫的時(shí)候都可以定位到特定的服務(wù)器,示意圖如圖1-3所示:


圖1-3:使用Hash方式的命中緩存

從上圖中旗笔,我們需要查詢的是圖product.png蝇恶,由于我們有6臺(tái)主服務(wù)器撮弧,所以計(jì)算的公式為:hash(product.png) % 6 = 5, 我們就可以定位到是5號(hào)主從姚糊,這們就省去了遍歷所有服務(wù)器的時(shí)間救恨,從而大大提升了性能肠槽。

使用Hash時(shí)遇到的問題

在上述hash取模的過程中署浩,我們雖然不需要對(duì)所有Redis服務(wù)器進(jìn)行遍歷而提升了性能。但是,使用Hash算法緩存時(shí)會(huì)出現(xiàn)一些問題弊攘,Redis服務(wù)器變動(dòng)時(shí)襟交,所有緩存的位置都會(huì)發(fā)生改變捣域。
比如,現(xiàn)在我們的Redis緩存服務(wù)器增加到了8臺(tái)迹鹅,我們計(jì)算的公式從hash(product.png) % 6 = 5變成了hash(product.png) % 8 = ? 結(jié)果肯定不是原來的5了斜棚。
再者弟蚀,6臺(tái)的服務(wù)器集群中义钉,當(dāng)某個(gè)主從群出現(xiàn)故障時(shí)断医,無法進(jìn)行緩存奏纪,那我們需要把故障機(jī)器移除序调,所以取模數(shù)又會(huì)從6變成了5发绢。我們計(jì)算的公式也會(huì)變化。

由于上面hash算法是使用取模來進(jìn)行緩存的经柴,為了規(guī)避上述情況坯认,Hash一致性算法就誕生了~~

一致性Hash算法原理

一致性Hash算法也是使用取模的方法牛哺,不過引润,上述的取模方法是對(duì)服務(wù)器的數(shù)量進(jìn)行取模痒玩,而一致性的Hash算法是對(duì)2的32方取模。即别凹,一致性Hash算法將整個(gè)Hash空間組織成一個(gè)虛擬的圓環(huán)番川,Hash函數(shù)的值空間為0 ~ 2^32 - 1(一個(gè)32位無符號(hào)整型)颁督,整個(gè)哈希環(huán)如下:

圖1-4:Hash圓環(huán)

整個(gè)圓環(huán)以順時(shí)針方向組織沉御,圓環(huán)正上方的點(diǎn)代表0吠裆,0點(diǎn)右側(cè)的第一個(gè)點(diǎn)代表1烂完,以此類推试疙。
第二步,我們將各個(gè)服務(wù)器使用Hash進(jìn)行一個(gè)哈希抠蚣,具體可以選擇服務(wù)器的IP或主機(jī)名作為關(guān)鍵字進(jìn)行哈希祝旷,這樣每臺(tái)服務(wù)器就確定在了哈希環(huán)的一個(gè)位置上,比如我們有三臺(tái)機(jī)器嘶窄,使用IP地址哈希后在環(huán)空間的位置如圖1-4所示:
圖1-4:服務(wù)器在哈希環(huán)上的位置

現(xiàn)在怀跛,我們使用以下算法定位數(shù)據(jù)訪問到相應(yīng)的服務(wù)器:

將數(shù)據(jù)Key使用相同的函數(shù)Hash計(jì)算出哈希值,并確定此數(shù)據(jù)在環(huán)上的位置柄冲,從此位置沿環(huán)順時(shí)針查找,遇到的服務(wù)器就是其應(yīng)該定位到的服務(wù)器现横。

例如漓拾,現(xiàn)在有ObjectA,ObjectB长赞,ObjectC三個(gè)數(shù)據(jù)對(duì)象晦攒,經(jīng)過哈希計(jì)算后,在環(huán)空間上的位置如下:


圖1-5:數(shù)據(jù)對(duì)象在環(huán)上的位置

根據(jù)一致性算法得哆,Object -> NodeA,ObjectB -> NodeB, ObjectC -> NodeC

一致性Hash算法的容錯(cuò)性和可擴(kuò)展性

現(xiàn)在哟旗,假設(shè)我們的Node C宕機(jī)了贩据,我們從圖中可以看到栋操,A、B不會(huì)受到影響饱亮,只有Object C對(duì)象被重新定位到Node A矾芙。所以我們發(fā)現(xiàn),在一致性Hash算法中近上,如果一臺(tái)服務(wù)器不可用剔宪,受影響的數(shù)據(jù)僅僅是此服務(wù)器到其環(huán)空間前一臺(tái)服務(wù)器之間的數(shù)據(jù)(這里為Node C到Node B之間的數(shù)據(jù)),其他不會(huì)受到影響壹无。如圖1-6所示:


圖1-6:C節(jié)點(diǎn)宕機(jī)情況葱绒,數(shù)據(jù)移到節(jié)點(diǎn)A上

另外一種情況,現(xiàn)在我們系統(tǒng)增加了一臺(tái)服務(wù)器Node X斗锭,如圖1-7所示:


圖1-7:增加新的服務(wù)器節(jié)點(diǎn)X

此時(shí)對(duì)象ObjectA地淀、ObjectB沒有受到影響,只有Object C重新定位到了新的節(jié)點(diǎn)X上岖是。
如上所述:

一致性Hash算法對(duì)于節(jié)點(diǎn)的增減都只需重定位環(huán)空間中的一小部分?jǐn)?shù)據(jù)帮毁,有很好的容錯(cuò)性和可擴(kuò)展性。

數(shù)據(jù)傾斜問題

在一致性Hash算法服務(wù)節(jié)點(diǎn)太少的情況下豺撑,容易因?yàn)楣?jié)點(diǎn)分布不均勻面造成數(shù)據(jù)傾斜(被緩存的對(duì)象大部分緩存在某一臺(tái)服務(wù)器上)問題烈疚,如圖1-8特例:

圖1-8:數(shù)據(jù)傾斜

這時(shí)我們發(fā)現(xiàn)有大量數(shù)據(jù)集中在節(jié)點(diǎn)A上,而節(jié)點(diǎn)B只有少量數(shù)據(jù)聪轿。為了解決數(shù)據(jù)傾斜問題爷肝,一致性Hash算法引入了虛擬節(jié)點(diǎn)機(jī)制,即對(duì)每一個(gè)服務(wù)器節(jié)點(diǎn)計(jì)算多個(gè)哈希屹电,每個(gè)計(jì)算結(jié)果位置都放置一個(gè)此服務(wù)節(jié)點(diǎn)阶剑,稱為虛擬節(jié)點(diǎn)。
具體操作可以為服務(wù)器IP或主機(jī)名后加入編號(hào)來實(shí)現(xiàn)危号,實(shí)現(xiàn)如圖1-9所示:
圖1-9:增加虛擬節(jié)點(diǎn)情況

數(shù)據(jù)定位算法不變牧愁,只需要增加一步:虛擬節(jié)點(diǎn)到實(shí)際點(diǎn)的映射。
所以加入虛擬節(jié)點(diǎn)之后外莲,即使在服務(wù)節(jié)點(diǎn)很少的情況下猪半,也能做到數(shù)據(jù)的均勻分布。

具體實(shí)現(xiàn)

算法接口類
public interface IHashService {
    Long hash(String key);
}
算法接口實(shí)現(xiàn)類
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;

    }
}
模擬機(jī)器節(jié)點(diǎn)
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當(dāng)做hash的Key
     *
     * @return String
     */
    @Override
    public String toString() {
        return ip;
    }
}
一致性Hash操作
public class ConsistentHash<T> {
    // Hash函數(shù)接口
    private final IHashService iHashService;
    // 每個(gè)機(jī)器節(jié)點(diǎn)關(guān)聯(lián)的虛擬節(jié)點(diǎn)數(shù)量
    private final int          numberOfReplicas;
    // 環(huán)形虛擬節(jié)點(diǎn)
    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);
        }
    }

    /**
     * 增加真實(shí)機(jī)器節(jié)點(diǎn)
     *
     * @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);
        }
    }

    /**
     * 刪除真實(shí)機(jī)器節(jié)點(diǎn)
     *
     * @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);

        // 沿環(huán)的順時(shí)針找到一個(gè)虛擬節(jié)點(diǎn)
        if (!circle.containsKey(hash)) {
            SortedMap<Long, T> tailMap = circle.tailMap(hash);
            hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
        }
        return circle.get(hash);
    }
}
測(cè)試類
public class TestHashCircle {
    // 機(jī)器節(jié)點(diǎn)IP前綴
    private static final String IP_PREFIX = "192.168.0.";

    public static void main(String[] args) {
        // 每臺(tái)真實(shí)機(jī)器節(jié)點(diǎn)上保存的記錄條數(shù)
        Map<String, Integer> map = new HashMap<String, Integer>();

        // 真實(shí)機(jī)器節(jié)點(diǎn), 模擬10臺(tái)
        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();
        // 每臺(tái)真實(shí)機(jī)器引入100個(gè)虛擬節(jié)點(diǎn)
        ConsistentHash<Node<String>> consistentHash = new ConsistentHash<Node<String>>(iHashService, 500, nodes);

        // 將5000條記錄盡可能均勻的存儲(chǔ)到10臺(tái)機(jī)器節(jié)點(diǎn)上
        for (int i = 0; i < 5000; i++) {
            // 產(chǎn)生隨機(jī)一個(gè)字符串當(dāng)做一條記錄偷线,可以是其它更復(fù)雜的業(yè)務(wù)對(duì)象,比如隨機(jī)字符串相當(dāng)于對(duì)象的業(yè)務(wù)唯一標(biāo)識(shí)
            String data = UUID.randomUUID().toString() + i;
            // 通過記錄找到真實(shí)機(jī)器節(jié)點(diǎn)
            Node<String> node = consistentHash.get(data);
            // 再這里可以能過其它工具將記錄存儲(chǔ)真實(shí)機(jī)器節(jié)點(diǎn)上磨确,比如MemoryCache等
            // ...
            // 每臺(tái)真實(shí)機(jī)器節(jié)點(diǎn)上保存的記錄條數(shù)加1
            map.put(node.getIp(), map.get(node.getIp()) + 1);
        }

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

運(yùn)行結(jié)果如下:


一致性hash測(cè)試結(jié)果

每臺(tái)機(jī)器映射的虛擬節(jié)點(diǎn)越多,則分布的越均勻~~~
感興趣的同學(xué)可以拷貝上面的代碼運(yùn)行嘗試一下声邦。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末乏奥,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子亥曹,更是在濱河造成了極大的恐慌邓了,老刑警劉巖恨诱,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異骗炉,居然都是意外死亡照宝,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門句葵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來厕鹃,“玉大人,你說我怎么就攤上這事乍丈〖敛辏” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵诗赌,是天一觀的道長汗茄。 經(jīng)常有香客問我,道長铭若,這世上最難降的妖魔是什么洪碳? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮叼屠,結(jié)果婚禮上瞳腌,老公的妹妹穿的比我還像新娘。我一直安慰自己镜雨,他們只是感情好嫂侍,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著荚坞,像睡著了一般挑宠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上颓影,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天各淀,我揣著相機(jī)與錄音,去河邊找鬼诡挂。 笑死碎浇,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的璃俗。 我是一名探鬼主播奴璃,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼城豁!你這毒婦竟也來了苟穆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鞭缭,沒想到半個(gè)月后剖膳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體魏颓,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡岭辣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了甸饱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片沦童。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖叹话,靈堂內(nèi)的尸體忽然破棺而出偷遗,到底是詐尸還是另有隱情,我是刑警寧澤驼壶,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布氏豌,位于F島的核電站,受9級(jí)特大地震影響热凹,放射性物質(zhì)發(fā)生泄漏泵喘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一般妙、第九天 我趴在偏房一處隱蔽的房頂上張望纪铺。 院中可真熱鬧,春花似錦碟渺、人聲如沸鲜锚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽芜繁。三九已至,卻和暖如春绒极,著一層夾襖步出監(jiān)牢的瞬間骏令,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國打工集峦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留伏社,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓塔淤,卻偏偏與公主長得像摘昌,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子高蜂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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