程序员修神之路--分布式缓存的一条明路(附代码)
菜菜呀,由于公司業務不斷擴大,線上分布式緩存服務器扛不住了呀
程序員主力 Y總如果加硬件能解決的問題,那就不需要修改程序
菜菜我是想加服務器來解決這個問題,但是有個問題呀
程序員主力 Y總???
菜菜你忘了去年分布式緩存服務器也擴容過一次,很多請求都穿透了,DB差點扛不住呀,這次再擴容DB估計就得掛了
程序員主力 Y總為什么會有這么多請求穿透呢?公司的緩存策略是什么?
菜菜很簡單,根據緩存數據key的哈希值然后和緩存服務器個數取模,即:服務器信息=hash(key)%服務器數量
程序員主力 Y總這樣的話,增加一臺服務器,豈不是大部分的緩存幾乎都命中不了了?
菜菜給你半天,把這個機制優化一下,你要加油呀
程序員主力 Y總工資能不能漲一點?
菜菜將來公司發達了,給你發股票......
程序員主力 Y總心想:呸!!
菜菜又是一個沒有開工紅包的公司!!!問題分析
????????過以上對話,各位是否能夠猜到所有緩存穿透的原因呢?回答之前我們先來看一下緩存策略的具體代碼:
????????
????這里還要多說一句,key的取值可以根據具體業務具體設計。比如,我想要做負載均衡,key可以為調用方的服務器IP;獲取用戶信息,key可以為用戶ID;等等。
????????在服務器數量不變的情況下,以上設計沒有問題。但是要知道,程序員的現實世界是悲慘的,唯一不變的就是業務一直在變。我本無奈,只能靠技術來改變這種狀況。
????????假如我們現在服務器的數量為10,當我們請求key為6的時候,結果是4,現在我們增加一臺服務器,服務器數量變為11,當再次請求key為6的服務器的時候,結果為5.不難發現,不光是key為6的請求,幾乎大部分的請求結果都發生了變化,這就是我們要解決的問題, 這也是我們設計分布式緩存等類似場景時候主要需要注意的問題。
我們終極的設計目標是:在服務器數量變動的情況下
1. 盡量提高緩存的命中率(轉移的數據最少)
2. 緩存數據盡量平均分配
解決方案
????????通過以上的分析我們明白了,造成大量緩存失效的根本原因是公式分母的變化,如果我們把分母保持不變,基本上可以減少大量數據被移動
????????如果基于公式:緩存服務器IP=hash(key)%服務器數量 我們保持分母不變,基本上可以改善現有情況。我們選擇緩存服務器的策略會變為:
????????N的數值選擇,可以根據具體業務選擇一個滿足情況的值。比如:我們可以肯定將來服務器數量不會超過100臺,那N完全可以設定為100。那帶來的問題呢?
?????????目前的情況可以認為服務器編號是連續的,任何一個請求都會命中一個服務器,還是以上作為例子,我們服務器現在無論是10還是增加到11,key為6的請求總是能獲取到一臺服務器信息,但是現在我們的策略公式分母為100,如果服務器數量為11,key為20的請求結果為20,編號為20的服務器是不存在的。
?????????以上就是簡單哈希策略帶來的問題(簡單取余的哈希策略可以抽象為連續的數組元素,按照下標來訪問的場景)
?為了解決以上問題,業界早已有解決方案,那就是一致性哈希。
一致性哈希具體的特點,請各位百度,這里不在詳細介紹。至于解決問題的思路這里還要強調一下:
1.? 首先求出服務器(節點)的哈希值,并將其配置到環上,此環有2^32個節點。
2.? 采用同樣的方法求出存儲數據的鍵的哈希值,并映射到相同的圓上。
3.? 然后從數據映射到的位置開始順時針查找,將數據保存到找到的第一個服務器上。如果超過2^32仍然找不到服務器,就會保存到第一臺服務器上
當增加新的服務器的時候會發生什么情況呢?
????通過上圖我們可以發現發生變化的只有如黃色部分所示。刪除服務器情況類似。
????通過以上介紹,一致性哈希正是解決我們目前問題的一種方案。解決方案千萬種,能解決問題即為好
優化方案
????????到目前為止方案都看似完美,但現實是殘酷的。以上方案雖好,但還存在瑕疵。假如我們有3臺服務器,理想狀態下服務器在哈希環上的分配如下圖:
但是現實往往是這樣:
????????這就是所謂的哈希環偏斜。分布不均勻在某些場景下會依次壓垮服務器,實際生產環境一定要注意這個問題。為了解決這個問題,虛擬節點應運而生。
????????如上圖,哈希環上不再是實際的服務器信息,而是服務器信息的映射信息,比如:ServerA-1,ServerA-2 都映射到服務器A,在環上是服務器A的一個復制品。這種解決方法是利用數量來達到均勻分布的目的,隨之需要的內存可能會稍微大一點,算是空間換取設計的一種方案。
擴展閱讀
1.? 既然是哈希就會有哈希沖突,那多個服務器節點的哈希值相同該怎么辦呢?我們可以采用散列表尋址的方案:從當前位置順時針開始查找空位置,直到找到一個空位置。如果未找到,菜菜認為你的哈希環是不是該擴容了,或者你的分母參數是不是太小了呢。
2.? 在實際的業務中,增加服務器或者減少服務器的操作要比查找服務器少的多,所以我們存儲哈希環的數據結構的查找速度一定要快,具體說來本質是:自哈希環的某個值起,能快速查找第一個不為空的元素。
3.? 如果你度娘過你就會發現,網上很多介紹虛擬哈希環節點個數為2^32(2的32次方),千篇一律。難道除了這個個數就不可以嗎?在菜菜看來,這個數目完全必要這么大,只要符合我們的業務需求,滿足業務數據即可。
4.? 一致性哈希用到的哈希函數,不止要保證比較高的性能,還要保持哈希值的盡量平均分布,這也是一個工業級哈希函數的要求,一下代碼實例的哈希函數其實不是最佳的,有興趣的同學可以優化一下。
5.? 有些語言自帶的GetHashCode()方法應用于一致性哈希是有問題的,例如c#。程序重啟之后同一個字符串的哈希值是變動的。所有需要一個更加穩定的字符串轉int的哈希算法
一致性哈希解決的本質問題是:相同的key通過相同的哈希函數,能正確路由到相同的目標。像我們平時用的數據庫分表策略,分庫策略,負載均衡,數據分片等都可以用一致性哈希來解決。
理論結合實際才是真諦(NetCore代碼)
以下代碼經過少許修改可直接應用于中小項目生產環境。
?//真實節點的信息????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);
????????}
????????
????????//從虛擬環的某個位置查找第一個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;
????????}
????????
????}
測試生成的節點
????????????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#410====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以上代碼實有優化空間
1. 哈希函數
2. 很多for循環的臨時變量
有興趣優化的同學可以留言哦!!
●程序員修仙之路--高性能排序多個文件
●程序員修仙之路--把用戶訪問記錄優化到極致
●程序員修仙之路--設計一個實用的線程池●程序員修仙之路--數據結構之CXO讓我做一個計算器●程序猿修仙之路--數據結構之設計高性能訪客記錄系統●程序猿修仙之路--算法之快速排序到底有多快●程序猿修仙之路--數據結構之你是否真的懂數組?●程序猿修仙之路--算法之希爾排序!
●程序員修仙之路--算法之插入排序!
●程序員修仙之路--算法之選擇排序!
互聯網之路,菜菜與君一同成長
長按識別二維碼關注
聽說轉發文章
會給你帶來好運
總結
以上是生活随笔為你收集整理的程序员修神之路--分布式缓存的一条明路(附代码)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: IIS作为ASP.NET Core2.1
- 下一篇: Asp.NetCore轻松学-部署到 I