經過以上對話,各位是否可以猜到全部緩存穿透的緣由呢?回答以前咱們先來看一下緩存策略的具體代碼node
緩存服務器IP=hash(key)%服務器數量
這裏還要多說一句,key的取值能夠根據具體業務具體設計。好比,我想要作負載均衡,key能夠爲調用方的服務器IP;獲取用戶信息,key能夠爲用戶ID;等等。程序員
在服務器數量不變的狀況下,以上設計沒有問題。可是要知道,程序員的現實世界是悲慘的,惟一不變的就是業務一直在變。我本無奈,只能靠技術來改變這種情況。算法
假如咱們如今服務器的數量爲10,當咱們請求key爲6的時候,結果是4,如今咱們增長一臺服務器,服務器數量變爲11,當再次請求key爲6的服務器的時候,結果爲5.不難發現,不光是key爲6的請求,幾乎大部分的請求結果都發生了變化,這就是咱們要解決的問題, 這也是咱們設計分佈式緩存等相似場景時候主要須要注意的問題。數據庫
咱們終極的設計目標是:在服務器數量變更的狀況下c#
經過以上的分析咱們明白了,形成大量緩存失效的根本緣由是公式分母的變化,若是咱們把分母保持不變,基本上能夠減小大量數據被移動數組
若是基於公式:緩存服務器IP=hash(key)%服務器數量 咱們保持分母不變,基本上能夠改善現有狀況。咱們選擇緩存服務器的策略會變爲:緩存
緩存服務器IP=hash(key)%N (N爲常數)
N的數值選擇,能夠根據具體業務選擇一個知足狀況的值。好比:咱們能夠確定未來服務器數量不會超過100臺,那N徹底能夠設定爲100。那帶來的問題呢?
目前的狀況能夠認爲服務器編號是連續的,任何一個請求都會命中一個服務器,仍是以上做爲例子,咱們服務器如今不管是10仍是增長到11,key爲6的請求老是能獲取到一臺服務器信息,可是如今咱們的策略公式分母爲100,若是服務器數量爲11,key爲20的請求結果爲20,編號爲20的服務器是不存在的。安全
以上就是簡單哈希策略帶來的問題(簡單取餘的哈希策略能夠抽象爲連續的數組元素,按照下標來訪問的場景)服務器
爲了解決以上問題,業界早已有解決方案,那就是一致性哈希。數據結構
一致性哈希算法在1997年由麻省理工學院的Karger等人在解決分佈式Cache中提出的,設計目標是爲了解決因特網中的熱點(Hot spot)問題,初衷和CARP十分相似。一致性哈希修正了CARP使用的簡單哈希算法帶來的問題,使得DHT能夠在P2P環境中真正獲得應用。
一致性哈希具體的特色,請各位百度,這裏不在詳細介紹。至於解決問題的思路這裏還要強調一下:
當增長新的服務器的時候會發生什麼狀況呢?
經過上圖咱們能夠發現發生變化的只有如黃色部分所示。刪除服務器狀況相似。
經過以上介紹,一致性哈希正是解決咱們目前問題的一種方案。解決方案千萬種,能解決問題即爲好。
到目前爲止方案都看似完美,但現實是殘酷的。以上方案雖好,但還存在瑕疵。假如咱們有3臺服務器,理想狀態下服務器在哈希環上的分配以下圖:
可是現實每每是這樣:
這就是所謂的哈希環偏斜。分佈不均勻在某些場景下會依次壓垮服務器,實際生產環境必定要注意這個問題。爲了解決這個問題,虛擬節點應運而生。
如上圖,哈希環上再也不是實際的服務器信息,而是服務器信息的映射信息,好比:ServerA-1,ServerA-2 都映射到服務器A,在環上是服務器A的一個複製品。這種解決方法是利用數量來達到均勻分佈的目的,隨之須要的內存可能會稍微大一點,算是空間換取設計的一種方案。
一致性哈希解決的本質問題是:相同的key經過相同的哈希函數,能正確路由到相同的目標。像咱們平時用的數據庫分表策略,分庫策略,負載均衡,數據分片等均可以用一致性哈希來解決。
如下代碼通過少量修改可直接應用於中小項目生產環境
//真實節點的信息 public abstract class NodeInfo { public abstract string NodeName { get; } }
測試程序所用節點信息:
class Server : NodeInfo { public string IP { get; set; } public override string NodeName { get => IP; } }
如下爲一致性哈希核心代碼:
/// <summary> /// 1.採用虛擬節點方式 2.節點總數能夠自定義 3.每一個物理節點的虛擬節點數能夠自定義 /// </summary> public class ConsistentHash { //哈希環的虛擬節點信息 public class VirtualNode { public string VirtualNodeName { get; set; } public NodeInfo Node { get; set; } } //添加元素 刪除元素時候的鎖,來保證線程安全,或者採用讀寫鎖也能夠 private readonly object objLock = new object(); //虛擬環節點的總數量,默認爲100 int ringNodeCount; //每一個物理節點對應的虛擬節點數量 int virtualNodeNumber; //哈希環,這裏用數組來存儲 public VirtualNode[] nodes = null; public ConsistentHash(int _ringNodeCount = 100, int _virtualNodeNumber = 3) { if (_ringNodeCount <= 0 || _virtualNodeNumber <= 0) { throw new Exception("_ringNodeCount和_virtualNodeNumber 必須大於0"); } this.ringNodeCount = _ringNodeCount; this.virtualNodeNumber = _virtualNodeNumber; nodes = new VirtualNode[_ringNodeCount]; } //根據一致性哈希key 獲取node信息,查找操做請業務方自行處理超時問題,由於多線程環境下,環的node可能全被清除 public NodeInfo GetNode(string key) { var ringStartIndex = Math.Abs(GetKeyHashCode(key) % ringNodeCount); var vNode = FindNodeFromIndex(ringStartIndex); return vNode == null ? null : vNode.Node; } //虛擬環添加一個物理節點 public void AddNode(NodeInfo newNode) { var nodeName = newNode.NodeName; int virtualNodeIndex = 0; lock (objLock) { //把物理節點轉化爲虛擬節點 while (virtualNodeIndex < virtualNodeNumber) { var vNodeName = $"{nodeName}#{virtualNodeIndex}"; var findStartIndex = Math.Abs(GetKeyHashCode(vNodeName) % ringNodeCount); var emptyIndex = FindEmptyNodeFromIndex(findStartIndex); if (emptyIndex < 0) { // 已經超出設置的最大節點數 break; } nodes[emptyIndex] = new VirtualNode() { VirtualNodeName = vNodeName, Node = newNode }; virtualNodeIndex++; } } } //刪除一個虛擬節點 public void RemoveNode(NodeInfo node) { var nodeName = node.NodeName; int virtualNodeIndex = 0; List<string> lstRemoveNodeName = new List<string>(); while (virtualNodeIndex < virtualNodeNumber) { lstRemoveNodeName.Add($"{nodeName}#{virtualNodeIndex}"); virtualNodeIndex++; } //從索引爲0的位置循環一遍,把全部的虛擬節點都刪除 int startFindIndex = 0; lock (objLock) { while (startFindIndex < nodes.Length) { if (nodes[startFindIndex] != null && lstRemoveNodeName.Contains(nodes[startFindIndex].VirtualNodeName)) { nodes[startFindIndex] = null; } startFindIndex++; } } } //哈希環獲取哈希值的方法,由於系統自帶的gethashcode,重啓服務就變了 protected virtual int GetKeyHashCode(string key) { var sh = new SHA1Managed(); byte[] data = sh.ComputeHash(Encoding.Unicode.GetBytes(key)); return BitConverter.ToInt32(data, 0); } #region 私有方法 //從虛擬環的某個位置查找第一個node private VirtualNode FindNodeFromIndex(int startIndex) { if (nodes == null || nodes.Length <= 0) { return null; } VirtualNode node = null; while (node == null) { startIndex = GetNextIndex(startIndex); node = nodes[startIndex]; } return node; } //從虛擬環的某個位置開始查找空位置 private int FindEmptyNodeFromIndex(int startIndex) { while (true) { if (nodes[startIndex] == null) { return startIndex; } var nextIndex = GetNextIndex(startIndex); //若是索引回到原地,說明找了一圈,虛擬環節點已經滿了,不會添加 if (nextIndex == startIndex) { return -1; } startIndex = nextIndex; } } //獲取一個位置的下一個位置索引 private int GetNextIndex(int preIndex) { int nextIndex = 0; //若是查找的位置到了環的末尾,則從0位置開始查找 if (preIndex != nodes.Length - 1) { nextIndex = preIndex + 1; } return nextIndex; } #endregion }
ConsistentHash h = new ConsistentHash(200, 5); h.AddNode(new Server() { IP = "192.168.1.1" }); h.AddNode(new Server() { IP = "192.168.1.2" }); h.AddNode(new Server() { IP = "192.168.1.3" }); h.AddNode(new Server() { IP = "192.168.1.4" }); h.AddNode(new Server() { IP = "192.168.1.5" }); for (int i = 0; i < h.nodes.Length; i++) { if (h.nodes[i] != null) { Console.WriteLine($"{i}===={h.nodes[i].VirtualNodeName}"); } }
輸出結果(還算比較均勻):
2====192.168.1.3#4 10====192.168.1.1#0 15====192.168.1.3#3 24====192.168.1.2#2 29====192.168.1.3#2 33====192.168.1.4#4 64====192.168.1.5#1 73====192.168.1.4#3 75====192.168.1.2#0 77====192.168.1.1#3 85====192.168.1.1#4 88====192.168.1.5#4 117====192.168.1.4#1 118====192.168.1.2#4 137====192.168.1.1#1 152====192.168.1.2#1 157====192.168.1.5#2 158====192.168.1.2#3 159====192.168.1.3#0 162====192.168.1.5#0 165====192.168.1.1#2 166====192.168.1.3#1 177====192.168.1.5#3 185====192.168.1.4#0 196====192.168.1.4#2
Stopwatch w = new Stopwatch(); w.Start(); for (int i = 0; i < 100000; i++) { var aaa = h.GetNode("test1"); } w.Stop(); Console.WriteLine(w.ElapsedMilliseconds);
輸出結果(調用10萬次耗時657毫秒):
657
以上代碼實有優化空間
有興趣優化的同窗能夠留言哦!!
添加關注,查看更精美版本,收穫更多精彩