天天看點

一緻性Hash算法Java版實作

本文已被Github倉庫收錄 https://github.com/silently9527/JavaCore
微信公衆号:貝塔學Java

前言

在之前寫了兩篇關于緩存的文章《萬字長文聊緩存(上)- http緩存》《萬字長文聊緩存(下)- 應用級緩存》,談到緩存不說一下一緻性Hash算法那就是在耍流氓。

分布式緩存叢集的通路模型

現在通常使用Redis來做分布式緩存,下面我們就以Redis為例:

一緻性Hash算法Java版實作

假如目前我們系統的業務發展很快,需要緩存的資料很多,是以我們做了一個由三組主從複制的redis組成的高可用的redis叢集,如何将請求路由的不同的redis叢集上,這是我們需要考慮的,常用的路由算法:

随機算法:每次将請求随機的發送到其中一組Redis叢集中,這種算法的好處是請求會被均勻的分發到每組Redis叢集上;缺點也很明顯,由于随機分發請求,為了提高緩存的命中率,是以同一份資料需要在每組叢集中都存在,這樣就會造成了資料的備援,浪費了存儲空間

Hash算法:針對随機算法的問題,我們可以考慮Hash算法,舉例:

現在有三組redis叢集,我們可以對每次緩存key的hash值取模,公式:

index=hash(key) % 3

,index的值就對應着3組叢集,這樣就可以保證同一個請求每次都被分發到同一個redis叢集上,無需對資料做備援,完美的解決了剛才随機算法的缺點;

一緻性Hash算法Java版實作

但是hash算法也有缺點:對于容錯性和伸縮性支援很差,舉例:當我們三組redis叢集中其中一組節點當機了,那麼此時的redis叢集中可用的數量變成了2,公式變成了

index=hash(key) % 2

, 所有資料緩存的節點位置就發生了變化,造成緩存的命中率直線下降;

同理,當我們需要擴充一組新的redis機器,計算的公式

index=hash(key) % 4

,大量的key會被重新定位到其他伺服器,也會造成緩存的命中率下降。

為了解決hash算法容錯性和伸縮性的問題,一緻性hash算法由此而生~

一緻性雜湊演算法

具體的算法過程

  1. 先構造一個長度為2^32-1的整數環(稱為一緻性hash環),然後給每組redis叢集命名,根據名字的hash值計算出每組叢集應該放在什麼位置
一緻性Hash算法Java版實作
  1. 根據緩存資料的key計算出hash值,計算出出來的hash值同樣也分布在一緻性hash環上; 假如現在有5個資料需要緩存對應的key分别為key1、key2、key3、key4、key5,計算hash值之後的分部如下圖
一緻性Hash算法Java版實作
  1. 然後順着hash環順時針方向查找reids叢集,把資料存放到最近的叢集上
一緻性Hash算法Java版實作

最後所有key4、key5存放在了叢集2,key1、key3存放在了叢集1,key2存放在了叢集3

容錯性

還是繼續沿用上面的例子,我們來看下一緻性雜湊演算法的容錯性如何呢?假如其中 叢集1 跪了,那麼影響的資料隻有key1和key3,其他資料存放的位置不受影響;當再次緩存key1、key3的時候根據順時針查找,會把資料存放到叢集3上面

伸縮性

如果我們需要在目前的基礎上再添加一組redis叢集4,根據名字hash之後的位置在叢集1和叢集2之間

一緻性Hash算法Java版實作

新加redis叢集4之後影響的隻有key1資料,其他資料不受影響。

資料傾斜問題

經過容錯性、伸縮性的驗證證明了一緻性雜湊演算法确實能解決Hash算法的問題,但是現在的算法就是完美的嗎?讓我們繼續來看剛才容錯性的例子,加入叢集1跪了,那麼原來落在叢集1上的所有資料會直接落在叢集3上面,如果說每組redis叢集的配置都是一樣的,那麼叢集3的壓力會增大,資料分布不均勻造成資料傾斜問題。

一緻性Hash算法Java版實作

怎麼搞呢?

歪果仁的腦子就是好使,給的解決方案就是加一層虛拟層,假如每組叢集都配置設定了2個虛拟節點

叢集 虛拟節點
叢集1 vnode1, vnode2
叢集2 vnode3, vnode4
叢集3 vnode5, vnode6

接下來就是把虛拟節點放入到一緻性hash環上,在緩存資料的時候根據順時針查找虛拟節點,在根據虛拟節點的和實際叢集的對應關系把資料存放到redis叢集,這樣資料就會均勻的分布到各組叢集中。

一緻性Hash算法Java版實作

這時候如果有一組redis叢集出現了問題,那麼這組叢集上面的key會相對均勻的分攤到其他叢集上。

從上面的結果來看,隻要每組叢集對應的虛拟節點越多,那麼各個實體叢集的資料分布越均勻,當新增加或者減少實體叢集影響也會最小,但是如果虛拟節點太多會影響查找的性能,太少資料又會不均勻,那麼多少合适呢?根據一些大神的經驗給出的建議是 150 個虛拟節點。

一緻性Hash算法Java版實作

實作思路:在每次添加實體節點的時候,根據實體節點的名字生成虛拟節點的名字,把虛拟節點的名字求hash值,然後把hash值作為key,實體節點作為value存放到Map中;這裡我們選擇使用TreeMap,因為需要key是順序的存儲;在計算資料key需要存放到哪個實體節點時,先計算出key的hash值,然後調用TreeMap.tailMap()傳回比hash值大的map子集,如果子集為空就需要把TreeMap的第一個元素傳回,如果不為空,那麼取子集中的第一個元素。

> 不扯廢話,直接上代碼,No BB . Show me the code

核心代碼:

一緻性Hash算法Java版實作
一緻性Hash算法Java版實作

測試代碼:

一緻性Hash算法Java版實作
  1. 測試删除節點node3,對比命中率影響了多少 添加如下代碼:
一緻性Hash算法Java版實作

執行結果:

一緻性Hash算法Java版實作
  1. 測試添加節點node5,對比命中率影響了多少 添加如下代碼:
一緻性Hash算法Java版實作

執行結果:

一緻性Hash算法Java版實作

其他使用場景

一緻性Hash算法Java版實作

看上圖,在Nginx請求的分發過程中,為了讓應用本地的緩存命中率最高,我們希望根據請求的URL或者URL參數将相同的請求轉發到同一個應用伺服器中,這個時候也可以選擇使用一緻性hash算法。具體配置可以參考官方文檔:

https://www.nginx.com/resources/wiki/modules/consistent_hash/

一緻性Hash算法Java版實作

寫到最後(點關注,不迷路)

文中或許會存在或多或少的不足、錯誤之處,有建議或者意見也非常歡迎大家在評論交流。

最後,白嫖不好,創作不易,希望朋友們可以點贊評論關注三連,因為這些就是我分享的全部動力來源🙏

原創不易 轉載請注明出處:https://mp.weixin.qq.com/s/eCxGPqrfIeFY_E_CnFRfMw