10分鐘了解一致性hash演算法
- 2019 年 10 月 3 日
- 筆記
應用場景
當我們的數據表超過500萬條或更多時,我們就會考慮到採用分庫分表;當我們的系統使用了一台快取伺服器還是不能滿足的時候,我們會使用多台快取伺服器,那我們如何去訪問背後的庫表或快取伺服器呢,我們肯定不會使用循環或者隨機了,我們會在存取的時候使用相同的哈希演算法定位到具體的位置。
簡單的哈希演算法
我們可以根據某個欄位(比如id)取模,然後將數據分散到不同的資料庫或表中。
例如前期規劃,我們某個業務數據5個庫就能滿足了,根據id取模 如下圖
我們通過hash取模很方便的路由到對應的庫上,但是上述的簡單的hash演算法還是有一些缺陷的,假如,5個庫也無法滿足業務的時候,我們需要9個庫,那麼原來的取模公式mod 5要變成 mod 9了,並且大部分數據都要重新分布,涉及到數據轉移工作量也是巨大的。有沒有一勞永逸的方法,答案是有的一致性hash演算法
一致性哈希演算法
演算法概述
一致性哈希演算法(Consistent Hashing),是MIT的karge及其合作者在1997年發表的學術論文提出的,最早在論文《Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web》中被提出。簡單來說,一致性哈希將整個哈希值空間組織成一個虛擬的圓環,如假設某哈希函數H的值空間為0 – 2^32-1(即哈希值是一個32位無符號整形),整個哈希空間環如下:
伺服器(ip或者主機名)本身進行哈希,確認每台機器在哈希環上的位置,例如ip:192.168.4.101,192.168.4.102,192.168.4.103 分別對應節點node1-101,node2-102,node3-103 如圖
數據key使用相同的函數計算出哈希值h,根據h確定此數據在環上的位置,從此位置沿環順時針“行走”,最近的伺服器就是其應該定位到的伺服器。 例如 我們使用”10″,”11″,”12″,”13″,”14″ 四個數據對象對應key10,key11,key12,key13,key14,經過哈希計算後,在環空間的位置如下:
根據一致性哈希演算法,數據key10,key14會被定位到節點node3-103上,key12,key13被定位到節點node1-101上,而key11會被定位到節點node2-102上。
擴展性
節點添加
如果我們新增一個節點node4-104 對應的ip:192.168.4.104通過對應的哈希演算法得到哈希值,並映射到環中,如下圖
通過按順時針遷移的規則,那麼key10被遷移到了node4-104中,其它數據還保持這原有的存儲位置
節點刪除
如果刪除一個節點node3-103,那麼按照順時針遷移的方法,key10,key14將會被遷移到node1-101上,其它的對象沒有任何的改動。如下圖:
如果服務節點太少的時候,會出現數據分配不均,比如極端情況下所有數據都落到node1-101節點上,如何解決數據傾斜問題,需要引入虛擬節點
虛擬節點
如果節點比較少的情況下,在0到2^32-1形成的環中,會出每個節點存放的數據不均勻;一致性哈希演算法提出虛擬節點的解決方案。即虛擬節點時實際節點(物理機器)在hash環中的複製品,一個實際節點對應N多個虛擬節點,這個對應個數也成為了複製個數,虛擬節點在hash環中以hash值排列。
例如 我們以刪除了一個點,只剩下 node1 和node2 兩個節點的圖;我們添加4個虛擬節點,兩個節點 則對應8個節點,最後映射關係 如圖
核心程式碼
public class KetamaNodeLocator { private SortedList<long, string> ketamaNodes = new SortedList<long, string>(); private HashAlgorithm hashAlg; private int numReps = 160; public KetamaNodeLocator(List<string> nodes, int nodeCopies) { ketamaNodes = new SortedList<long, string>(); numReps = nodeCopies; //對所有節點,生成nCopies個虛擬結點 foreach (string node in nodes) { //每四個虛擬結點為一組 for (int i = 0; i < numReps / 4; i++) { //getKeyForNode方法為這組虛擬結點得到惟一名稱 byte[] digest = HashAlgorithm.computeMd5(node + i); /** Md5是一個16位元組長度的數組,將16位元組的數組每四個位元組一組,分別對應一個虛擬結點,這就是為什麼上面把虛擬結點四個劃分一組的原因*/ for (int h = 0; h < 4; h++) { long m = HashAlgorithm.hash(digest, h); ketamaNodes[m] = node; } } } } public string GetPrimary(string k) { byte[] digest = HashAlgorithm.computeMd5(k); string rv = GetNodeForKey(HashAlgorithm.hash(digest, 0)); return rv; } string GetNodeForKey(long hash) { string rv; long key = hash; //如果找到這個節點,直接取節點,返回 if (!ketamaNodes.ContainsKey(key)) { //得到大於當前key的那個子Map,然後從中取出第一個key,就是大於且離它最近的那個key 說明詳見: http://www.javaeye.com/topic/684087 var tailMap = from coll in ketamaNodes where coll.Key > hash select new { coll.Key }; if (tailMap == null || tailMap.Count() == 0) key = ketamaNodes.FirstOrDefault().Key; else key = tailMap.FirstOrDefault().Key; } rv = ketamaNodes[key]; return rv; } }
public class HashAlgorithm { public static long hash(byte[] digest, int nTime) { long rv = ((long)(digest[3 + nTime * 4] & 0xFF) << 24) | ((long)(digest[2 + nTime * 4] & 0xFF) << 16) | ((long)(digest[1 + nTime * 4] & 0xFF) << 8) | ((long)digest[0 + nTime * 4] & 0xFF); return rv & 0xffffffffL; /* Truncate to 32-bits */ } /** * Get the md5 of the given key. */ public static byte[] computeMd5(string k) { MD5 md5 = new MD5CryptoServiceProvider(); byte[] keyBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(k)); md5.Clear(); //md5.update(keyBytes); //return md5.digest(); return keyBytes; }
最後貼上了實現程式碼,可以運行跑跑,加深理解,希望對您有所幫助,碼字不易請多多支援。
參考
代震軍—-https://www.cnblogs.com/daizhj/archive/2010/08/24/1807324.html