有圖有真相
原理:根據特定的字符串比如5臺服務器ip的數組,按照一定放大倍數比如4000倍,哈希映射出20000個哈希值,他們的關係存到ketamaNides數組中,每臺服務器,對應相同等份的4000個哈希值,
現在有一批量字符串數據,比如10000條要存進來,如何決定它存到哪臺服務器呢?
這個hash值和根據服務器ip映射出來的hash值,可以比較大小,
根據ketamaNodes找到距離最近的一個服務器ip映射出來的hash值,進而知道該服務器名稱,
重點是根據hash定義,不管它是什麼字符串,都可以映射成等長的hash值,
博主這種說法也許並不專業,但是絕對淺顯易懂,幫助理解是沒問題的!
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace HashSetDemo
{
public class KetamaNodeLocator
{
private SortedList<long, string> ketamaNodes = new SortedList<long, string>();
private HashAlgorithm hashAlg;
private int numReps = 4000;
public List<string> services;
//每臺服務器負載,和具體數據沒多大關係,
public int TestItem = 10000;
public KetamaNodeLocator(List<string> nodes/*,int nodeCopies*/)
{
services = nodes;
ketamaNodes = new SortedList<long, string>();
//numReps = nodeCopies;
//對所有節點,生成nCopies個虛擬結點
//方便理解,就認爲有四個物理節點
foreach (string node in nodes)
{
//每四個虛擬結點爲一組
//40組,160節點
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;
}
}
}
//關鍵關鍵的是,每個ketamaNodes存,hash值和每臺物理機器ip
//只是爲何不更簡單點,直接A_1_1 A_1_2這樣劃分一百六十個虛擬節點呢
}
public string GetPrimary(string k)
{
byte[] digest = HashAlgorithm.computeMd5(k);
string rv = GetNodeForKey(HashAlgorithm.hash(digest, 0));
return rv;
}
public void AddServer(string nodename)
{
//循環muti次數,給一個名字,生成不同的hash值,即一個hash環
//同一個名字,產生不同的hash值散佈關聯
services.Add(nodename);
//每四個虛擬結點爲一組
//40組,160節點
for (int i = 0; i < numReps / 4; i++)
{
//getKeyForNode方法爲這組虛擬結點得到惟一名稱
//每一個物理節點,需要生成四十個虛擬節點
byte[] digest = HashAlgorithm.computeMd5(nodename + i);
/** Md5是一個16字節長度的數組,將16字節的數組每四個字節一組,分別對應一個虛擬結點,這就是爲什麼上面把虛擬結點四個劃分一組的原因*/
//具體給每一個虛擬節點生成一個哈希值,
//四十個組,每組再循環四組
for (int h = 0; h < 4; h++)
{
long m = HashAlgorithm.hash(digest, h);
ketamaNodes[m] = nodename;
}
}
Console.WriteLine("新增:" + nodename);
}
//刪除某臺服務器,刪除對應所有的hash
public void RemoveServer(string nodename)
{
//每四個虛擬結點爲一組
services.Remove(nodename);
//40組,160節點
for (int i = 0; i < numReps / 4; i++)
{
//getKeyForNode方法爲這組虛擬結點得到惟一名稱
//每一個物理節點,需要生成四十個虛擬節點
byte[] digest = HashAlgorithm.computeMd5(nodename + i);
/** Md5是一個16字節長度的數組,將16字節的數組每四個字節一組,分別對應一個虛擬結點,這就是爲什麼上面把虛擬結點四個劃分一組的原因*/
//具體給每一個虛擬節點生成一個哈希值,
//四十個組,每組再循環四組
for (int h = 0; h < 4; h++)
{
long m = HashAlgorithm.hash(digest, h);
ketamaNodes.Remove(m);
}
}
Console.WriteLine("移除:"+nodename);
}
string GetNodeForKey(long hash)
{
string rv;
long key = hash;
//如果找到這個節點,直接取節點,返回
if (!ketamaNodes.ContainsKey(key))
{
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 void Print()
{
Console.WriteLine($"當前服務器個數:{services.Count},放大倍數:{numReps},總節點數:{services.Count*numReps}");
//單獨存放每臺服務器中添加數據條數,
SortedList<string,int> data= new SortedList<string, int>();
//存多少條數據進入,根據hash算法,存到那臺服務器,但是
//160放大倍數,形成將近一百六十個節點,會不會數據過大,全部落在外面呢?即
//有可能很多比最小值小,那麼放在第一臺數量將會很多,同理很多比最大值大,放在最後一臺數量要多
for (int i = 0; i < TestItem; i++)
{
//!!!!!注意kelata裏面的hash都是通過以下兩步獲取對應的hash,後面nTime默認給0即可
string vname=GetNodeForKey(HashAlgorithm.hash(HashAlgorithm.computeMd5($"我是要存的數據{i}"),0));
//通過kelataNodes集合,找到數據,該存入的服務器名稱,
//一定能找到麼?儘管這個值不一定等於已存在的某個key,不過仍然能找到大於這個hash的下一個key,所對應的服務器
//,1-3-5,7-9-11,如果是8大於它就是9放在第二臺服務器
if (data.ContainsKey(vname))
{
data[vname] += 1;
}
else {
data[vname] = 1;
}
}
//
Console.WriteLine($"存入的數據條數:{TestItem}");
foreach (var item in data)
{
Console.WriteLine($"{item.Key},承載數據{((float)item.Value/(float)TestItem*100).ToString("0.00")}%");
}
}
}
public class HashAlgorithm
{
//這是一種hash算法
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;
}
}
}
KetamaNodeLocator kn = new KetamaNodeLocator(new List<string>() { "我是1號服務器", "我是2號服務器", "我是3號服務器", "我是4號服務器", "我是5號服務器"});
kn.Print();
Console.WriteLine("\r\n");
kn.AddServer("我是6號服務器");
kn.Print();
Console.WriteLine("\r\n");
kn.RemoveServer("我是1號服務器");
kn.Print();
Console.ReadLine();