一致性hash算法及其java實現

背景

隨著業務系統越來越大,我們需要對API的訪問進行更多的緩存,使用Redis是一個很好的解決方案.
但是單臺Redis性能不足夠且遲早要走向集群的,那么怎么才能良好的利用Redis集群來進行緩存呢?
當一個請求到來,我們如何決定將這個請求的內容緩存在那臺Redis服務器上?我們一一道來.

分配方法

隨機分配

假設我們有X臺服務器,當一個請求來到的時候,我們獲取一個0-X的隨機數,然后將內容緩存在該服務器上.
這明顯是不可選的,想要查詢的時候我們自己也不知道在哪,只能逐個遍歷服務器,知道拿到為止.

hash取模

還有一種常見的方式就是對集群數量進行hash取模.比如我們現在有3臺服務器,那么對請求的key進行hash,之后拿到的hashcode對3進行取模,得到的數字就是該key應該存儲的服務器.
這樣雖然解決了上面的獲取問題,但是擴展性極其差,設想一下現在我們需要新添加一臺機器,也就是機器數量來到了4,那么對4取模的結果和對3取模的結果基本上全部不一樣,也就是說我們需要對所有的key進行一次重新的hash計算并重新存儲.

一致性hash

這也是我們今天的重點,它于1997年由麻省理工學院提出.我們在下面單獨講解一下他.

一致性hash原理

其實本質上,一致性hash也是hash取模,只是是永遠的對2的32次方-1取模.

一致性hash引入了一個叫做一致性hash環的概念,即將(0-2^32-1)中間的所有整數首尾相接連接成一個環.如下圖:



然后將所有的節點映射到環上,假設我們有3個節點,N1,N2.N3.那么如下圖:



之后我們將要存儲的所有key也都映射到環上,假設我們有6個key.

這樣之后,順時針旋轉key,將其存儲在遇到的第一個服務器上,這樣有什么好處呢?

那就是擴展性,當新插入一個節點時,只會影響到少部分key,需要重新計算的key很少,我們添加一個節點試試:



可以發現,只有N3數據需要從N2節點遷移到N4.
是不是看起來挺美滋滋的,啥好處都有,有啥缺點呢?
缺點當然有.

1上面的圖是一種理想狀態,基本算是均勻的分布了,但是實際使用中,你用一個集群中的機器名(有很大的可能性很類似)去hash,拿到的結果可能很相近,也就是說,并不是像圖中這樣分散的,而是聚集在一起,而key是分散的,這樣會導致,大量的key命中了其中一個或者多個服務器,而有一部分卻空閑.總之,負載不均衡.

2redis的key都是字符串,而字符串的hashcode方法是可能會返回負值的,而一致性hash環是只有正值的,因此需要我們使用別的hash算法.(淡然你也可以粗暴的進行取絕對值).

使用虛擬節點解決hash不均勻的問題

hash不均勻主要出現在節點很少的時候,那么我們可以手動模擬一些節點出來,也就是所謂的虛擬節點,比如我們只有3個節點,但是我們定義一個規則,比如A-1,A-2,A-3,這三個節點都可以被映射到環上,但是在真正存儲的時候我們都存儲在A上.



只要我們的虛擬節點足夠多,我們就可以讓其盡可能的均勻分布在環上.

總結

一致性hash算法是使用虛擬的環狀數據結構,解決了簡單hash算法中擴展性差的問題,在分布式緩存以及負載均衡中有許多的應用.

Java實現一致性hash算法緩存客戶端

1、Java中提供了ConcurrentSkipListMap類,可以很好的使用在這里,不僅可以輕松的模擬環狀結構,并發安全且使用跳表結構的ConcurrentSkipListMap可以提供很好的并發性能.

2、對于虛擬節點的多少,其實是可以大概估算出來的,因此在下面的代碼中,我將其作為一個變量,在初始化的時候由當前節點的數量計算得到,當然我沒有具體實現計算方法.這么設計是出于什么考慮呢,想讓虛擬節點的數量盡量的剛剛好,萬一節點很多,還是用固定的虛擬節點,對均勻性提升不會很大,反而會造成性能損耗等.

3、代碼中主要提供了一下幾個方法:

初始化,用一個redis配置的字符串
添加和刪除節點,會將其虛擬節點一起操作.
jedis的get和set操作,當然在實際情況下不會只有這兩個方法,這里只做模擬,對更多的方法沒有做一個實現.

好了,廢話不多說了,都在注釋里面了!

package util;

import redis.clients.jedis.Jedis;

import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;

/**
 * Created by pfliu on 2019/05/19.
 */
public class ConsistentHashRedis {

    // 用跳表模擬一致性hash環,即使在節點很多的情況下,也可以有不錯的性能
    private final ConcurrentSkipListMap<Integer, String> circle;
    // 虛擬節點數量
    private final int virtual_size;

    public ConsistentHashRedis(String configs) {
        this.circle = new ConcurrentSkipListMap<>();
        String[] cs = configs.split(",");
        this.virtual_size = getVirtualSize(cs.length);
        for (String c : cs) {
            this.add(c);
        }
    }

    /**
     * 將每個節點添加進環中,并且添加對應數量的虛擬節點
     */
    private void add(String c) {
        if (c == null) return;
        for (int i = 0; i < virtual_size; ++i) {
            String virtual = c + "-N" + i;
            int hash = getHash(virtual);
            circle.put(hash, virtual);
        }
    }

    // 根據字符串獲取hash值,這里使用簡單粗暴的絕對值.
    private int getHash(String s) {
        return Math.abs(s.hashCode());
    }

    // 計算當前需要多少個虛擬節點,這里沒有計算,直接使用了150.
    private int getVirtualSize(int length) {
        return 150;
    }

    /**
     * 對外提供的set方法
     */
    public void set(String key, String v) {
        getJedisFromCircle(key).set(key, v);
    }

    public String get(String k) {
        return getJedisFromCircle(k).get(k);
    }

    /**
     * 從環中取到適合當前key的jedis.
     */
    private Jedis getJedisFromCircle(String key) {
        int keyHash = getHash(key);
        ConcurrentNavigableMap<Integer, String> tailMap = circle.tailMap(keyHash);
        String config = tailMap.isEmpty() ? circle.firstEntry().getValue() : tailMap.firstEntry().getValue();
        // 注意,由于使用了虛擬節點,所以這里要做 虛擬節點 -> 真實節點的映射
        String[] cs = config.split("-");
        return new Jedis(cs[0]);
    }

    /**
     * 對外暴露的添加節點接口
     */
    public boolean addJedis(String cs) {
        add(cs);
        return true;
    }

    /**
     * 對外暴露的刪除節點節點
     */
    public boolean deleteJedis(String cs) {
        delete(cs);
        return true;
    }

    /**
     * 從環中刪除一個節點極其虛擬節點
     */
    private void delete(String cs) {
        if (cs == null) return;
        for (int i = 0; i < virtual_size; ++i) {
            String virtual = cs + "-N" + i;
            int hash = getHash(virtual);
            circle.remove(hash, virtual);
        }
    }
}

作者:呼延十
鏈接:https://juejin.im/post/5cfdf4e5f265da1bd260e04c
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容