本文已被Github倉庫收錄 https://github.com/silently9527/JavaCorenode
微信公衆號:貝塔學Javanginx
前言
在以前寫了兩篇關於緩存的文章《萬字長文聊緩存(上)- http緩存》《萬字長文聊緩存(下)- 應用級緩存》,談到緩存不說一下一致性Hash算法那就是在耍流氓。git
分佈式緩存集羣的訪問模型
如今一般使用Redis來作分佈式緩存,下面咱們就以Redis爲例:github
假如當前咱們系統的業務發展很快,須要緩存的數據不少,因此咱們作了一個由三組主從複製的redis組成的高可用的redis集羣,如何將請求路由的不一樣的redis集羣上,這是咱們須要考慮的,經常使用的路由算法:redis
隨機算法:每次將請求隨機的發送到其中一組Redis集羣中,這種算法的好處是請求會被均勻的分發到每組Redis集羣上;缺點也很明顯,因爲隨機分發請求,爲了提升緩存的命中率,因此同一份數據須要在每組集羣中都存在,這樣就會形成了數據的冗餘,浪費了存儲空間算法
Hash算法:針對隨機算法的問題,咱們能夠考慮Hash算法,舉例: 如今有三組redis集羣,咱們能夠對每次緩存key的hash值取模,公式:index=hash(key) % 3
,index的值就對應着3組集羣,這樣就能夠保證同一個請求每次都被分發到同一個redis集羣上,無需對數據作冗餘,完美的解決了剛纔隨機算法的缺點;緩存
可是hash算法也有缺點:對於容錯性和伸縮性支持不好,舉例:當咱們三組redis集羣中其中一組節點宕機了,那麼此時的redis集羣中可用的數量變成了2,公式變成了index=hash(key) % 2
, 全部數據緩存的節點位置就發生了變化,形成緩存的命中率直線降低;服務器
同理,當咱們須要擴展一組新的redis機器,計算的公式index=hash(key) % 4
,大量的key會被從新定位到其餘服務器,也會形成緩存的命中率降低。微信
爲了解決hash算法容錯性和伸縮性的問題,一致性hash算法由此而生~分佈式
一致性哈希算法
具體的算法過程
- 先構造一個長度爲2^32-1的整數環(稱爲一致性hash環),而後給每組redis集羣命名,根據名字的hash值計算出每組集羣應該放在什麼位置
- 根據緩存數據的key計算出hash值,計算出出來的hash值一樣也分佈在一致性hash環上; 假如如今有5個數據須要緩存對應的key分別爲key一、key二、key三、key四、key5,計算hash值以後的分部以下圖
- 而後順着hash環順時針方向查找reids集羣,把數據存放到最近的集羣上
最後全部key四、key5存放在了集羣2,key一、key3存放在了集羣1,key2存放在了集羣3
容錯性
仍是繼續沿用上面的例子,咱們來看下一致性哈希算法的容錯性如何呢?假如其中 集羣1 跪了,那麼影響的數據只有key1和key3,其餘數據存放的位置不受影響;當再次緩存key一、key3的時候根據順時針查找,會把數據存放到集羣3上面
伸縮性
若是咱們須要在當前的基礎上再添加一組redis集羣4,根據名字hash以後的位置在集羣1和集羣2之間
新加redis集羣4以後影響的只有key1數據,其餘數據不受影響。
數據傾斜問題
通過容錯性、伸縮性的驗證證實了一致性哈希算法確實能解決Hash算法的問題,可是如今的算法就是完美的嗎?讓咱們繼續來看剛纔容錯性的例子,加入集羣1跪了,那麼原來落在集羣1上的全部數據會直接落在集羣3上面,若是說每組redis集羣的配置都是同樣的,那麼集羣3的壓力會增大,數據分佈不均勻形成數據傾斜問題。
怎麼搞呢?
歪果仁的腦子就是好使,給的解決方案就是加一層虛擬層,假如每組集羣都分配了2個虛擬節點
集羣 | 虛擬節點 |
---|---|
集羣1 | vnode1, vnode2 |
集羣2 | vnode3, vnode4 |
集羣3 | vnode5, vnode6 |
接下來就是把虛擬節點放入到一致性hash環上,在緩存數據的時候根據順時針查找虛擬節點,在根據虛擬節點的和實際集羣的對應關係把數據存放到redis集羣,這樣數據就會均勻的分佈到各組集羣中。
這時候若是有一組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
核心代碼:
測試代碼:
- 測試刪除節點node3,對比命中率影響了多少 添加以下代碼:
執行結果:
- 測試添加節點node5,對比命中率影響了多少 添加以下代碼:
執行結果:
其餘使用場景
看上圖,在Nginx請求的分發過程當中,爲了讓應用本地的緩存命中率最高,咱們但願根據請求的URL或者URL參數將相同的請求轉發到同一個應用服務器中,這個時候也能夠選擇使用一致性hash算法。具體配置能夠參考官方文檔: https://www.nginx.com/resources/wiki/modules/consistent_hash/
寫到最後(點關注,不迷路)
文中或許會存在或多或少的不足、錯誤之處,有建議或者意見也很是歡迎你們在評論交流。
最後,白嫖很差,創做不易,但願朋友們能夠點贊評論關注三連,由於這些就是我分享的所有動力來源🙏
原創不易 轉載請註明出處:https://mp.weixin.qq.com/s/eCxGPqrfIeFY_E_CnFRfMw