首頁>技術>

一、簡介

二、傳統hash演算法的問題

在快取場景,以結點取模為例,假設有5個快取結點,即資料存入及讀取分佈這5個結點上,請求hash值為x,那麼請求對應的結點為:x%5=(0,1,2,3,4)。

當新增1個結點 此時結點個數變為6,那麼請求hash值對應的結點變為x%6=(0,1,2,3,4,5),以前hash值為5對應結點0,現在是hash值為6才對應結點0,其它hash值對應的結點類似,對應的相關操作如資料存入及讀取,都會發生改變,結點上的資料也隨著都會發生改變。影響太大。

三、一致性hash演算法特點

一致性hash演算法擁有均衡性(balance)、單調性(monotonicity)、分散性(spread)、負載(load)這些特點。最終表現,即是當有結點加入或刪除時,除少數結點外,大多數結點的對映關係不會改變,且新增結點時,請求要麼到新結點,要麼到原結點,而不會到其它舊結點,刪除時,請求到其它結點,只是刪除結點對應的原請求對映關係改變,其它結點對應的請求對映關係不會改變 。

均衡性(balance) 均衡性是指hash結果儘可能均衡分散在所有結點中。

單調性(monotonicity) 單調性是指某hash值一旦對映到某結點,當有新結點加入時,該hash值要麼對映到原結點,要麼對映到新加的結點,而不會對映到其它舊結點。

分散性(spread) 分散性是指分散式場景中,不同終端可能見不到所有所有輸出結點,進而相同hash值對映的結點可能不同。這需要避免。

負載(load) 負載是從另一角度看分散性,指同一輸出結點,不能被不同終端儲存不同的內容。

四、一致性hash演算法設計

一致性hash演算法設計,是將請求和輸出結點都對映到抽象的hash環上,結點查詢時,請求hash值在抽象hash環上,按順時針查詢結點,遇到的第一個結點即為輸出結點。

hash環 這是抽象的環,請求和結點按指的hash演算法,都對映到hash環上。

虛擬結點 為了更均衡地將請求對映到所有結點,可以將結點虛擬為多個虛擬結點,這樣各個結點在hash環上分佈的更均衡,相應的處理請求也更均衡。

五、一致性hash演算法的java實現

這裡使用java來實現一致性hash演算法,使用TreeMap作為hash環的資料結構,使用MurmurHash作為hash演算法(藉助guava27.1-jre的jar包),具體請參考程式碼註釋。

import com.google.common.collect.Lists;

import com.google.common.hash.HashCode;

import com.google.common.hash.HashFunction;

import com.google.common.hash.Hashing;

import java.nio.charset.Charset;

import java.util.List;

import java.util.SortedMap;

import java.util.TreeMap;

import java.util.function.Consumer;

import java.util.function.Function;

import java.util.stream.Collectors;

import java.util.stream.Stream;

public class ConsistentHashAlgMain {

public static void main(String[] args) {

/ 環境模擬 /

//模擬物理機器結點

List<String> servers = Lists.newArrayList("192.168.0.1", "192.168.0.2", "192.168.0.3", "192.168.0.4", "192.168.0.5");

//模擬請求

List<String> reqs = Lists.newArrayList("request one", "request two", "request three", "request four", "request five");

//生成抽象的hash環(使用TreeMap作為hash環的資料結構)

TreeMap<String, String> serverRing = new TreeMap<>();

//指定hash演算法為murmur

HashFunction hf = Hashing.murmur3_128();

/ 輔助方法 /

//生成hash值方法

Function<String, HashCode> hash = t -> hf.hashString(t, Charset.forName("utf8"));

//生成虛擬結點字尾列表的方法

List<Integer> virNum = Stream.iterate(1, n -> n + 1).limit(100).collect(Collectors.toList());

//單臺物理機器生成虛擬機器器結點列表的方法

Function<String, List<String>> buildVirServer = s -> virNum.stream().map(t -> s + "#" + t).collect(Collectors.toList());

//新增一臺物理機器方法

Consumer<String> addServer = t -> {

//獲取物理機對應的虛擬機器器列表

List<String> virServers = buildVirServer.apply(t);

//將虛擬機器器列表加入到hash環中

virServers.forEach(v -> {

//獲取虛擬機器器的hash值

HashCode h = hash.apply(v);

//將虛擬機器器加入到hash環中

serverRing.put(h.toString(), t);

});

};

Consumer<String> delServer = t -> {

//獲取物理機對應的虛擬機器器列表

List<String> virServers = buildVirServer.apply(t);

virServers.forEach(v -> {

//獲取虛擬機器器的hash值

HashCode h = hash.apply(v);

serverRing.remove(h.toString());

});

};

//初始化物理機器(每臺物理機器會先生成多臺虛擬機器器列表)到hash環方法

Consumer<List<String>> initServerRing = list -> {

list.forEach(t -> addServer.accept(t));

};

/ 請求處理方法 /

//處理請求方法

Function<String, String> ringHashHost = t -> {

//獲取請求的hash值

HashCode h = hash.apply(t);

//查詢請求hash值後對應的剩餘hash環機器

SortedMap<String, String> tailRing = serverRing.tailMap(h.toString());

//獲取請求hash值後找到的第一臺機器的key

String idx = tailRing.size() < 1 ? serverRing.firstKey() : tailRing.firstKey();

//獲取請求hash值後找到的第一臺機器

String s = serverRing.get(idx);

System.out.println(t + " : " + s);

return s;

};

//批次處理請求方法

Consumer<List<String>> handleReqs = list -> {

list.forEach(t -> ringHashHost.apply(t));

};

/ 測試 /

//初始化hash環

initServerRing.accept(servers);

//處理請求

handleReqs.accept(reqs);

//新增新機器並處理請求

System.out.println("###### add server #####");

addServer.accept("192.168.0.6");

handleReqs.accept(reqs);

System.out.println("###### del server #####");

delServer.accept("192.168.0.4");

handleReqs.accept(reqs);

}

}

結果輸出:

request one : 192.168.0.5

request two : 192.168.0.5

request three : 192.168.0.3

request four : 192.168.0.4

request five : 192.168.0.1

\###### add server #####

request one : 192.168.0.5

request two : 192.168.0.5

request three : 192.168.0.6

request four : 192.168.0.4

request five : 192.168.0.1

\###### del server #####

request one : 192.168.0.5

request two : 192.168.0.5

request three : 192.168.0.6

request four : 192.168.0.1

request five : 192.168.0.1

六、一致性hash演算法的簡單實現

另外也可以藉助google的guava包提供的一致性hash演算法的基本實現,直接使用,其關鍵使用程式碼(在此不詳解)如下:

//所有結點

List<String> servers = Lists.newArrayList("192.168.0.1", "192.168.0.2", "192.168.0.3", "192.168.0.4", "192.168.0.5");

//請求對映到結點

int idx = Hashing.consistentHash(Hashing.sha256().hashString("request one", Charset.forName("utf-8")), servers.size());

15
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • spring boot中zookeeper使用