一致性hash算法及其java實現

目錄

背景

隨着業務系統愈來愈大,咱們須要對API的訪問進行更多的緩存,使用Redis是一個很好的解決方案.java

可是單臺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)中間的全部整數首尾相接鏈接成一個環.以下圖:

2019-05-19-21-52-53

而後將全部的節點映射到環上,假設咱們有3個節點,N1,N2.N3.那麼以下圖:

2019-05-19-21-56-52
.

以後咱們將要存儲的全部key也都映射到環上,假設咱們有6個key.

2019-05-19-22-30-41

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

那就是擴展性,當新插入一個節點時,只會影響到少部分key,須要從新計算的key不多,咱們添加一個節點試試:

2019-05-19-22-33-59

能夠發現,只有N3數據須要從N2節點遷移到N4.

是否是看起來挺美滋滋的,啥好處都有,有啥缺點呢?

缺點固然有.

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

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

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

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

2019-05-19-23-13-23

只要咱們的虛擬節點足夠多,咱們就可讓其儘量的均勻分佈在環上.

總結

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

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

  1. Java中提供了ConcurrentSkipListMap類,能夠很好的使用在這裏,不只能夠輕鬆的模擬環狀結構,併發安全且使用跳錶結構的ConcurrentSkipListMap能夠提供很好的併發性能.

  2. 對於虛擬節點的多少,實際上是能夠大概估算出來的,所以在下面的代碼中,我將其做爲一個變量,在初始化的時候由當前節點的數量計算獲得,固然我沒有具體實現計算方法.這麼設計是出於什麼考慮呢,想讓虛擬節點的數量儘可能的剛恰好,萬一節點不少,仍是用固定的虛擬節點,對均勻性提高不會很大,反而會形成性能損耗等.

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

    1. 初始化,用一個redis配置的字符串
    2. 添加和刪除節點,會將其虛擬節點一塊兒操做.
    3. 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);
        }
    }
}


複製代碼

完。



ChangeLog

2019-05-19 完成

**以上皆爲我的所思所得,若有錯誤歡迎評論區指正。**

歡迎轉載,煩請署名並保留原文連接。

聯繫郵箱:huyanshi2580@gmail.com

更多學習筆記見我的博客------>呼延十

相關文章
相關標籤/搜索