1. 程式人生 > 實用技巧 >一致性雜湊演算法C#實現

一致性雜湊演算法C#實現

一致性hash實現,以下實現沒有考慮多執行緒情況,也就是沒有加鎖,需要的可以自行加上。因為換行的問題,閱讀不太方便,可以拷貝到本地再讀。

  1 /// <summary>
  2 /// 一致性雜湊。
  3 /// </summary>
  4 public static class ConsistentHashing
  5 {
  6     /// <summary>
  7     /// 虛擬節點倍數
  8     /// </summary>
  9     private static int _virtualNodeMultiple = 100;
 10 
 11     ///
<summary> 12 /// 真實節點資訊 13 /// </summary> 14 private static readonly List<string> Nodes = new List<string>(); 15 16 /// <summary> 17 /// 虛擬節點資訊(int型別主要是為了獲取虛擬節點時的二分查詢) 18 /// </summary> 19 private static readonly List<int> VirtualNode = new
List<int>(); 20 21 /// <summary> 22 /// 虛擬節點和真實節點對映,在獲取到虛擬節點之後,能以O(1)的時間複雜度返回真實節點 23 /// </summary> 24 private static readonly Dictionary<int, string> VirtualNodeAndNodeMap = new Dictionary<int, string>(); 25 26 /// <summary> 27 /// 增加節點 28 ///
</summary> 29 /// <param name="hosts">節點集合</param> 30 /// <returns>操作結果</returns> 31 public static bool AddNode(params string[] hosts) 32 { 33 if (hosts == null || hosts.Length == 0) 34 { 35 return false; 36 } 37 Nodes.AddRange(hosts); //先將節點增加到真實節點資訊中。 38 foreach (var item in hosts) 39 { 40 for (var i = 1; i <= _virtualNodeMultiple; i++) //此處迴圈為類似“192.168.3.1”這樣的真實ip字串從1加到1000,算作虛擬節點。192.168.3.11,192.168.3.11000 41 { 42 var currentHash = HashAlgorithm.GetHashCode(item + i) & int.MaxValue; //計算一個hash,此處用自定義hash演算法原因是字串預設的雜湊實現不保證對同一字串獲取hash時得到相同的值。和int.MaxValue進行位與操作是為了將獲取到的hash值設定為正數 43 if (!VirtualNodeAndNodeMap.ContainsKey(currentHash)) //因為hash可能會重複,如果當前hash已經包含在虛擬節點和真實節點對映中,則以第一次新增的為準,此處不再進行新增 44 { 45 VirtualNode.Add(currentHash);//將當前虛擬節點新增到虛擬節點中 46 VirtualNodeAndNodeMap.Add(currentHash, item);//將當前虛擬節點和真實ip放入對映中。 47 } 48 } 49 } 50 VirtualNode.Sort(); //操作完成之後進行一次對映,是為了後面根據key的hash值查詢虛擬節點時使用二分查詢。 51 return true; 52 } 53 54 /// <summary> 55 /// 移除節點 56 /// </summary> 57 /// <param name="host">指定節點</param> 58 /// <returns></returns> 59 public static bool RemoveNode(string host) 60 { 61 if (!Nodes.Remove(host)) //如果將指定節點從真實節點集合中移出失敗,後序操作不需要進行,直接返回 62 { 63 return false; 64 } 65 for (var i = 1; i <= _virtualNodeMultiple; i++) 66 { 67 var currentHash = HashAlgorithm.GetHashCode(host + i) & int.MaxValue; //計算一個hash,此處用自定義hash演算法原因是字串預設的雜湊實現不保證對同一字串獲取hash時得到相同的值。和int.MaxValue進行位與操作是為了將獲取到的hash值設定為正數 68 if (VirtualNodeAndNodeMap.ContainsKey(currentHash) && VirtualNodeAndNodeMap[currentHash] == host) //因為hash可能會重複,所以此處判斷在判斷了雜湊值是否存在於虛擬節點和節點對映中之後還需要判斷通過當前hash值獲取到的節點是否和指定節點一致,如果不一致,則證明這個這個虛擬節點不是當前hash值所擁有的 69 { 70 VirtualNode.Remove(currentHash); //從虛擬節點中移出 71 VirtualNodeAndNodeMap.Remove(currentHash); //從虛擬節點和真實ip對映中移出 72 } 73 } 74 VirtualNode.Sort(); //操作完成之後進行一次對映,是為了後面根據key的hash值查詢虛擬節點時使用二分查詢。 75 return true; 76 } 77 78 /// <summary> 79 /// 獲取所有節點 80 /// </summary> 81 /// <returns></returns> 82 public static List<string> GetAllNodes() 83 { 84 var nodes = new List<string>(Nodes.Count); 85 nodes.AddRange(Nodes); 86 return nodes; 87 } 88 89 /// <summary> 90 /// 獲取節點數量 91 /// </summary> 92 /// <returns></returns> 93 public static int GetNodesCount() 94 { 95 return Nodes.Count; 96 } 97 98 /// <summary> 99 /// 重新設定虛擬節點倍數 100 /// </summary> 101 /// <param name="multiple"></param> 102 public static void ReSetVirtualNodeMultiple(int multiple) 103 { 104 if (multiple < 0 || multiple == _virtualNodeMultiple) 105 { 106 return; 107 } 108 var nodes = new List<string>(Nodes.Count); 109 nodes.AddRange(Nodes); //將現有的真實節點拷貝出來 110 _virtualNodeMultiple = multiple; //設定倍數 111 Nodes.Clear(); 112 VirtualNode.Clear(); 113 VirtualNodeAndNodeMap.Clear(); //清空資料 114 AddNode(nodes.ToArray()); //重新新增 115 } 116 117 /// <summary> 118 /// 獲取節點 119 /// </summary> 120 /// <param name="key"></param> 121 /// <returns></returns> 122 public static string GetNode(string key) 123 { 124 var hash = HashAlgorithm.GetHashCode(key) & int.MaxValue; 125 var start = 0; 126 var end = VirtualNode.Count - 1; 127 while (end - start > 1) 128 { 129 var index = (start + end) / 2; 130 if (VirtualNode[index] > hash) 131 { 132 end = index; 133 } 134 else if (VirtualNode[index] < hash) 135 { 136 start = index; 137 } 138 else 139 { 140 start = end = index; 141 } 142 } 143 return VirtualNodeAndNodeMap[VirtualNode[start]]; 144 } 145 146 /// <summary> 147 /// hash 148 /// </summary> 149 private static class HashAlgorithm 150 { 151 public static int GetHashCode(string key) 152 { 153 return Hash(ComputeMd5(key)); 154 } 155 156 private static int Hash(byte[] digest, int nTime = 0) 157 { 158 long rv = ((long)(digest[3 + nTime * 4] & 0xFF) << 24) 159 | ((long)(digest[2 + nTime * 4] & 0xFF) << 16) 160 | ((long)(digest[1 + nTime * 4] & 0xFF) << 8) 161 | ((long)digest[0 + nTime * 4] & 0xFF); 162 return (int)(rv & 0xffffffffL); 163 } 164 private static byte[] ComputeMd5(string k) 165 { 166 MD5 md5 = new MD5CryptoServiceProvider(); 167 byte[] keyBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(k)); 168 md5.Clear(); 169 return keyBytes; 170 } 171 } 172 }
一致性hash實現

測試程式碼:

  1 class Program
  2 {
  3     static void Main(string[] args)
  4     {
  5         ConsistentHashing.AddNode(new[]
  6         {
  7         "192.168.137.1",
  8         "192.168.137.2",
  9         "192.168.137.3",
 10         "192.168.137.4",
 11         "192.168.137.5",
 12         "192.168.137.6",
 13         "192.168.137.7",
 14         "192.168.137.8",
 15         "192.168.137.9",
 16         "192.168.137.10"
 17     });
 18         var data = LoadTestData();
 19 
 20         Stopwatch stop = new Stopwatch();
 21         stop.Start();
 22         foreach (var item in data)
 23         {
 24             var node = ConsistentHashing.GetNode(item);
 25         }
 26         stop.Stop();
 27 
 28         var map10 = new Dictionary<string, string>();
 29         var mapCount10 = new Dictionary<string, int>();
 30 
 31         var map11 = new Dictionary<string, string>();
 32         var mapCount11 = new Dictionary<string, int>();
 33 
 34         var map9 = new Dictionary<string, string>();
 35         var mapCount9 = new Dictionary<string, int>();
 36 
 37         #region 10個節點
 38         foreach (var item in data)
 39         {
 40             var host = ConsistentHashing.GetNode(item);
 41             if (!map10.ContainsKey(item))
 42             {
 43                 map10.Add(item, host);
 44             }
 45             if (!mapCount10.ContainsKey(host))
 46             {
 47                 mapCount10.Add(host, 1);
 48             }
 49             else
 50             {
 51                 mapCount10[host]++;
 52             }
 53         }
 54         #endregion
 55 
 56         #region 11個節點
 57         ConsistentHashing.AddNode("192.168.137.11");
 58         foreach (var item in data)
 59         {
 60             var host = ConsistentHashing.GetNode(item);
 61             if (!map11.ContainsKey(item))
 62             {
 63                 map11.Add(item, host);
 64             }
 65             if (!mapCount11.ContainsKey(host))
 66             {
 67                 mapCount11.Add(host, 1);
 68             }
 69             else
 70             {
 71                 mapCount11[host]++;
 72             }
 73         }
 74         #endregion
 75 
 76         #region 9個節點
 77         ConsistentHashing.RemoveNode("192.168.137.11");
 78         ConsistentHashing.RemoveNode("192.168.137.10");
 79         foreach (var item in data)
 80         {
 81             var host = ConsistentHashing.GetNode(item);
 82             if (!map9.ContainsKey(item))
 83             {
 84                 map9.Add(item, host);
 85             }
 86             if (!mapCount9.ContainsKey(host))
 87             {
 88                 mapCount9.Add(host, 1);
 89             }
 90             else
 91             {
 92                 mapCount9[host]++;
 93             }
 94         }
 95         #endregion
 96 
 97         #region 資料比較和儲存
 98         var tenAndNine = 0;
 99         foreach (var item in map9)
100         {
101             if (map10[item.Key] != item.Value)
102             {
103                 tenAndNine++;
104             }
105         }
106         var tenAndEleven = 0;
107         foreach (var item in map11)
108         {
109             if (map10[item.Key] != item.Value)
110             {
111                 tenAndEleven++;
112             }
113         }
114         List<string> csv = new List<string>();
115         csv.Add("ip,10,10分佈,9,9分佈,11,11分佈");
116         foreach (var item in mapCount11)
117         {
118             var str = item.Key;
119             if (mapCount10.ContainsKey(item.Key))
120             {
121                 str += "," + mapCount10[item.Key];
122                 str += "," + (mapCount10[item.Key] / (double)100000).ToString("F2");
123             }
124             else
125             {
126                 str += ",";
127                 str += ",";
128             }
129             if (mapCount9.ContainsKey(item.Key))
130             {
131                 str += "," + mapCount9[item.Key];
132                 str += "," + (mapCount9[item.Key] / (double)100000).ToString("F2");
133             }
134             else
135             {
136                 str += ",";
137                 str += ",";
138             }
139             str += "," + mapCount11[item.Key];
140             str += "," + (mapCount11[item.Key] / (double)100000).ToString("F2");
141             csv.Add(str);
142         }
143         csv.Add(string.Format("10-1的失效資料:{0},比例:{2:F2}。10+1的失效資料:{1},比例:{3:F2}", tenAndNine, tenAndEleven, (tenAndNine / (double)1000000), (tenAndEleven / (double)1000000)));
144         File.WriteAllLines(@"E:\1000.csv", csv, Encoding.UTF8);
145         #endregion
146 
147         Console.ReadKey();
148     }
149 
150     /// <summary>
151     /// 生成測試key
152     /// </summary>
153     /// <param name="count"></param>
154     /// <returns></returns>
155     private static List<string> LoadTestData(int count = 1000000)
156     {
157         var data = new List<string>(count);
158 
159         for (var i = 0; i < count; i++)
160         {
161             data.Add(GetRandomString(15, true, true, true, false, ""));
162         }
163         return data;
164     }
165 
166     ///<summary>
167     ///生成隨機字串 
168     ///</summary>
169     ///<param name="length">目標字串的長度</param>
170     ///<param name="useNum">是否包含數字,1=包含,預設為包含</param>
171     ///<param name="useLow">是否包含小寫字母,1=包含,預設為包含</param>
172     ///<param name="useUpp">是否包含大寫字母,1=包含,預設為包含</param>
173     ///<param name="useSpe">是否包含特殊字元,1=包含,預設為不包含</param>
174     ///<param name="custom">要包含的自定義字元,直接輸入要包含的字元列表</param>
175     ///<returns>指定長度的隨機字串</returns>
176     public static string GetRandomString(int length, bool useNum, bool useLow, bool useUpp, bool useSpe, string custom)
177     {
178         byte[] b = new byte[4];
179         new System.Security.Cryptography.RNGCryptoServiceProvider().GetBytes(b);
180         Random r = new Random(BitConverter.ToInt32(b, 0));
181         string s = null, str = custom;
182         if (useNum == true) { str += "0123456789"; }
183         if (useLow == true) { str += "abcdefghijklmnopqrstuvwxyz"; }
184         if (useUpp == true) { str += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; }
185         if (useSpe == true) { str += "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; }
186         for (int i = 0; i < length; i++)
187         {
188             s += str.Substring(r.Next(0, str.Length - 1), 1);
189         }
190         return s;
191     }
192 }
測試程式碼

測試結果,圖中的分佈都是以10萬為基數: