閱讀目錄:node
以Redis爲例,當系統須要緩存的內容超過單機內存大小時,例如要緩存100G數據,單機內存僅有16G時。這時候就須要考慮進行緩存數據分片,也便是把100G的數據拆分紅多塊小於單機內存的數據。例如以10G爲單位,拆分10份,存儲到多臺機器節點上。 可是數據怎麼個分法更合理呢? 、redis
這裏配置n=10,不一樣的key根據數值餘數映射到對應的機器。 很簡單的辦法就解決了多臺節點key分法的問題。然而數據大小的增加和縮減是很難預知的, 若是須要增長一臺緩存服務器。 配置n=11,會發現以前根據餘數創建的映射關係發生混亂。映射錯亂後,就會發生大量key沒法命中正確的節點,須要所有從新進行映射。 若是之後再添加節點,一樣會遇到這樣問題。 算法
servers = ['redis:6379', 'redis:6380', 'redis:6381'] server = servers[f(key) % servers.length]
爲了下降添加或刪除服務器節點,致使大量key沒法命中的影響。就提出了一種更爲合理的分法,也便是一致性hash算法。 下面看下爲何更合理些?緩存
咱們在腦中假想下:每臺節點以CHash(ip)形式計算出一個數值,n臺機器有n個數值。 把數值首尾相連,造成一個虛擬圓環的數值空間。
例若有3臺機器:服務器
servers =['redis:6379', 'redis:6380', 'redis:6381']。 CHash(server[0])==100 Chash(server[1])==200 CHash(server[2])==300
把機器計算獲得的數值,在虛擬圓環中按照順時針方向來肯定空間歸屬,獲得:ide
100~200空間屬於6379管。
200~300空間屬於6380管。
300+,100-空間屬於6381管。函數
這時有3個key要存儲到redis,分別是key1—key3。 經過CHash函數計算出3個key的數值座標:spa
CHash(key1)=102 CHash(key2)=240 CHash(key3)=350
求出key的數值座標後,就知道key與機器節點的映射關係。 即key1應存儲在6379,key3存儲到6381。3d
因爲緩存數據的增長,須要添加一臺新節點6382。計算出空間數值:code
CHash(6382)==250
那麼他在虛擬圓環中的位置以下:
從圖中得知,637九、6381的數值空間區域沒任何變化,它們存儲的key依舊能夠正常命中的。
優勢之一:對現有緩存的命中影響較小。
但本來6380的區域200~300被6382侵入了。 6382的空間數值250正好劃分一半,即200~250的區域還歸6380管,但250~300的區域卻歸新來的管了。 (爲示例而使用簡單數字區分,實際上沒這麼精準)
優勢之二:實現對數據的分片
同時也帶來了缺點就是: 本來存儲在6380(250~300這部分)的舊緩存數據就沒法命中了,要去新的6382拿。 因此說一致性hash並不能徹底解決這種影響,只能儘可能下降。
與添加節點同理。好比拿掉新加的6382,250~300區域還管原來的6380管,固然6382這部分緩存也就丟了。
一致性Hash雖然實現了數據分片,但因爲節點較少,key有可能會大量集中到某一臺上面,致使緩存分佈不均勻。 特別是在只有幾臺或十幾臺機器節點時。
爲了下降這種影響,一致性hash算法提出虛擬節點的解決方案。 即一個物理機器節點對應着多個虛擬節點。 這裏配置一個物理節點對應2個虛擬節點,此時應爲:
6379={6379A,6379B} 6380={6380A,6380B} 6381={6381A,6381B}
這樣成了6個節點了(能夠配置更多),它們一樣在虛擬圓環上按數值順時針排列。因爲節點變多,對應的數值區域也變大。使key進行數值空間映射時變的更加離散性,從機率上來提升key的均勻分佈。
本來須要計算真實節點數值,也變成計算虛擬節點數值, 而後由虛擬節點的數值構成虛擬圓環數值空間。其中每一組虛擬節點數值,對應單個物理節點。
servers= ['redis:6379', 'redis:6380', 'redis:6381'];
//下面f函數中先將servers與虛擬節點映射成 6379={6379A,6379B}, 6380={6380A,6380B},6381={6381A,6381B}
// 在對虛擬節點求各自的數值,而數值對應的仍是物理節點。即:
vservers = f(servers) ={['redis:6379','100'],['redis:6379','300'] ....,['redis:6381',150]}; CHash(key1)==102 ∈ vservers[0] ...... CHash(key3)==350 ∈ vservers[1]
虛擬節點使key分佈的更加均衡,但不能解決添加機、刪除節點帶來的影響。
1:使用字典模擬虛擬圓環,並添加節點。
2:計算key數值,應該歸屬到哪一個節點數值空間區域。
3:計算分佈頻率。複製的虛擬節點越多,分佈越平均。
private static readonly SortedDictionary<ulong, string> _circle = new SortedDictionary<ulong, string>(); static void Main(string[] args) { int Replicas = 100; AddNode("127.0.0.1:6379", Replicas); AddNode("127.0.0.1:6380", Replicas); AddNode("127.0.0.1:6381", Replicas); List<string> nodes = new List<string>(); for (int i = 0; i < 100; i++) { nodes.Add(GetTargetNode(i + "test" + (char)i)); } var counts = nodes.GroupBy(n => n, n => n.Count()).ToList(); counts.ForEach(index => Console.WriteLine(index.Key+"-"+index.Count())); Console.ReadLine(); }
輸出:
127.0.0.1:6380-39
127.0.0.1:6381-29
127.0.0.1:6379-32
虛擬圓環的值:
其他代碼:
public static void AddNode(string node, int repeat) { for (int i = 0; i < repeat; i++) { string identifier = node.GetHashCode().ToString() + "-" + i; ulong hashCode = Md5Hash(identifier); _circle.Add(hashCode, node); } } public static ulong Md5Hash(string key) { using (var hash = System.Security.Cryptography.MD5.Create()) { byte[] data = hash.ComputeHash(Encoding.UTF8.GetBytes(key)); var a = BitConverter.ToUInt64(data, 0); var b = BitConverter.ToUInt64(data, 8); ulong hashCode = a ^ b; return hashCode; } } public static string GetTargetNode(string key) { ulong hash = Md5Hash(key); ulong firstNode = ModifiedBinarySearch(_circle.Keys.ToArray(), hash); return _circle[firstNode]; } /// <summary> /// 計算key的數值,得出空間歸屬。 /// </summary> /// <param name="sortedArray"></param> /// <param name="val"></param> /// <returns></returns> public static ulong ModifiedBinarySearch(ulong[] sortedArray, ulong val) { int min = 0; int max = sortedArray.Length - 1; if (val < sortedArray[min] || val > sortedArray[max]) return sortedArray[0]; while (max - min > 1) { int mid = (max + min) / 2; if (sortedArray[mid] >= val) { max = mid; } else { min = mid; } } return sortedArray[max]; }