首先,一致性哈希是對經典哈希的一個改造
經典的哈希方法使用哈希函數來生成偽隨機數,然后除以內存空間的大小,將隨機標識符轉變成可用空間內的一個位置
location = hash(key)mod size
在經典哈希方法中,我們總是假設:內存位置的數量是已知的,而且這個數永遠不變
但是這種哈希處理模型有個問題,size發生變更后,很多key對應的location都要發生變化,這對於海量數據存儲來說是災難
size變化后如何能盡量降低或減少location的變更呢?
那么一致性哈希出場
什么是一致性哈希
表示某種虛擬環結構(名為哈希環,HashRing),無論節點(硬件)或者請求(數據)都通過哈希算法后通過2的32次方進行hash取模0到2的32次方-1來確定哈希環的位置
這里我們和經典哈希做一個對比
classicHash = hash(key)mod size
ConsistentHash = hash(key)mod 2^32
變化1、不難看出變量size換成一個常量2^32(這個常量足夠大)
變化2、由經典的結果確定位置,變成了順時針范圍取值,經典哈希表大小的變更實際上干擾了所有映射,但一致性哈希並不會,這也就是一致性的原因
這樣做最大的好處就是當增、減節點(ABCD),只需要局部變動數據歸屬
比如BC間添加節點E,如果E的location在KEY3 4之間,那么只需要變動KEY3的歸屬即可
比如刪除D節點,也只需要KEY5的歸屬變更到A上
一致性哈希的特點
1、平衡性(Balance):平衡性是指哈希的結果能夠盡可能分布到所有的緩沖中去,這樣可以使得所有的緩沖空間都得到利用。很多哈希算法都能夠滿足這一條件。
我的理解,所有的請求都能進入的哈希環中
2、單調性(Monotonicity):單調性是指如果已經有一些數據通過哈希分配到了相應的機器上,又有新的機器加入到系統中。哈希的結果應能夠保證原有已分配的內容可以被映射到原有的或者新的機器中去,而不會被映射到舊的機器集合中的其他機器上。這里再解釋一下:就是原有的數據要么還是呆在它所在的機器上不動,要么被遷移到新的機器上,而不會遷移到舊的其他機器上。
我的理解,在BC間新增節點E,BC之間的數據要么保留歸屬C,要么變更為新增的節點E,不能歸屬給其他的原有節點(比如不能歸屬給ABD)
一致性哈希傾斜解決:虛擬節點
一致性哈希算法中上圖的情況是有可能出現的,即數據分部不均勻,發生了哈希傾斜,如何解決這個問題?
通過增設虛擬節點,即上圖中 A B C D4個節點,變更 A1 A2 B1 B2 C1 C2 D1 D2這些虛擬節點,他們並不是真正的物理節點
比如你歸屬於B2或B1都算歸屬於B,這樣會降低發生哈希傾斜的嚴重程度,但仍無法完全避免,下圖是上圖通過增設虛擬節點后哈希傾斜解決的比較好的一種情況
一致性哈希的應用
redis客戶端jedis實現的sharding方式,就是通過一致性哈希的方式對數據進行客戶端分片
rpc請求的負載均衡算法
所有分布式存儲分片
總結:請求->硬件的分配 存儲->硬件的分配
主要用他的意義通常要被分配者要支持變動對全局影響小(前提是被分配者變動影響全局,導致工作量很大)
應用一致性哈希
guava封裝了一致性哈希的操作
傳入多少桶,均勻的把桶分配到哈希環,傳入hash值后,算出key屬於哪個桶,加減桶后再算key屬於哪個桶
/** * 測試一致性哈希 */ @Test public void testConsistentHash(){ List<String> servers = Lists.newArrayList("server1", "server2", "server3", "server4", "server5" ,"server11", "server21", "server31", "server41", "server51" ,"server31", "server32", "server33", "server34", "server35" ,"server51", "server52", "server53", "server54", "server55"); List<String> userIds = Arrays.asList("zxp123549440","zxp","2222","asdhaksdhsakjdsa","!@#$%^"); System.out.println("=============目前有"+servers.size()+"個服務器"); userIds.forEach(userId -> { int bucket = Hashing.consistentHash(Hashing.murmur3_128().hashString(userId, Charset.forName("utf8")), servers.size()); System.out.println("userId:"+userId+" bucket:"+bucket); }); //去掉下標為17的server servers.remove(17); servers.remove(11); servers.remove(10); System.out.println("=============下線3台后還有"+servers.size()+"個服務器"); userIds.forEach(userId -> { int bucket = Hashing.consistentHash(Hashing.murmur3_128().hashString(userId, Charset.forName("utf8")), servers.size()); System.out.println("userId:"+userId+" bucket:"+bucket); }); } /** * 測試經典哈希 */ @Test public void testClassicHash(){ List<String> servers = Lists.newArrayList("server1", "server2", "server3", "server4", "server5" ,"server11", "server21", "server31", "server41", "server51" ,"server31", "server32", "server33", "server34", "server35" ,"server51", "server52", "server53", "server54", "server55"); List<String> userIds = Arrays.asList("zxp123549440","zxp","2222","asdhaksdhsakjdsa","!@#$%^"); System.out.println("=============目前有"+servers.size()+"個服務器"); userIds.forEach(userId -> { long bucket = Math.abs(Hashing.murmur3_128().hashString(userId, Charset.forName("utf8")).padToLong()) % servers.size(); System.out.println("userId:"+userId+" bucket:"+bucket); }); //去掉下標為17的server servers.remove(17); servers.remove(11); servers.remove(10); System.out.println("=============下線3台后還有"+servers.size()+"個服務器"); userIds.forEach(userId -> { long bucket = Math.abs(Hashing.murmur3_128().hashString(userId, Charset.forName("utf8")).padToLong()) % servers.size(); System.out.println("userId:"+userId+" bucket:"+bucket); }); }
ConsistentHash輸出結果 5個id,桶位置3個沒變 =============目前有20個服務器 userId:zxp123549440 bucket:3 userId:zxp bucket:3 userId:2222 bucket:10 userId:asdhaksdhsakjdsa bucket:18 userId:!@#$%^ bucket:0 =============下線3台后還有17個服務器 userId:zxp123549440 bucket:3 userId:zxp bucket:3 userId:2222 bucket:10 userId:asdhaksdhsakjdsa bucket:0 userId:!@#$%^ bucket:0 ClassicHash輸出結果 5個id,桶位置全變 =============目前有20個服務器 userId:zxp123549440 bucket:17 userId:zxp bucket:9 userId:2222 bucket:2 userId:asdhaksdhsakjdsa bucket:14 userId:!@#$%^ bucket:18 =============下線3台后還有17個服務器 userId:zxp123549440 bucket:2 userId:zxp bucket:4 userId:2222 bucket:10 userId:asdhaksdhsakjdsa bucket:9 userId:!@#$%^ bucket:1
如果場景切換到jedis的sharding方式,對比通過一致性哈希實現分片、經典哈希分片:
sharding接到set操作后,拿到key,通過murmur算法算出hash值,並從redis獲取目前可用的“集群”節點數,然后通過Hashing.consistentHash就能計算出key應該放入哪個桶,那么sharding用一致性哈希的優勢再哪里,假如掉線一個redis實例那么可能大部分key還能再原來的桶中查到,但是如果用的經典的哈希,那么絕大部分的key都變換了redis實例的位置,還是不如一致性哈希結果好