背景
618來臨之際,為了應對一些突發流量,購買了兩台一個月的ECS用來臨時對部分項目擴容。其中一個項目有用到雪花算法來生成Id,這個還是挺OK的。
不過發現要在配置文件中手動配置機器碼!!配置的時候還要先知道目前配置了那些,這樣才可以避免重復。
經過了解,除了會有單機單實例的情況,還會有單機多實例的情況。
這個要人工配置,是徒增工作量的,有點讓人難以接受。
針對這個,老黃就做了一點調整,讓這個機器碼自動生成。
雪花算法基礎
關於雪花算法,大部分文章都可以看到這個圖。這個圖很好的詮釋了雪花算法生成Id的幾個重要組成部分,這里也不展開具體介紹了。
時間戳,工作機器Id,序列號這些位數是可以根據自己的業務場景來進來調整的。
10bit工作機器Id,其實就是上面說到的機器碼,雪花算法內部並沒有做任何處理,而是交由業務方自己定義,所以業務方需要自己保證這個的唯一性。
大部分情況,會把它分為5bit數據中心標識和5bit機器Id。這樣的話可以支持32個數據中心和32個機器Id。
換句話說就是,一個業務可以在一個數據中心部署32個實例,最多部署的32個數據中心。正常來說,大部分項目,都不會需要部署這么多實例。。。
考慮到內網的IP段基本上是固定的,同一個應用基本上也會在連續的IP上面部署。
所以這里老黃最后采用的是本地IP地址取余做為機器Id,機器的HostName取余做為默認的數據中心Id。
下面來看看具體的實現。
簡單實現
自動獲取機器Id和數據中心Id。
/// <summary>
/// 獲取機器Id
/// </summary>
/// <returns></returns>
private int GetWorkerId()
{
var workerId = 0;
try
{
IPAddress ipaddress = IPAddress.Parse("0.0.0.0");
NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces();
foreach (NetworkInterface ni in interfaces)
{
if (ni.NetworkInterfaceType == NetworkInterfaceType.Ethernet)
{
foreach (UnicastIPAddressInformation ip in
ni.GetIPProperties().UnicastAddresses)
{
if (ip.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
{
ipaddress = ip.Address;
break;
}
}
}
}
Console.WriteLine($"ip = {ipaddress.ToString()}");
var val = ipaddress.GetAddressBytes().Sum(x => x);
// 取余
workerId = val % (int)MaxWorkerId;
}
catch
{
// 異常的話,生成一個隨機數
workerId = new Random().Next((int)MaxWorkerId - 1);
}
return workerId;
}
/// <summary>
/// 獲取數據中心Id
/// </summary>
/// <returns></returns>
private int GetDatacenterId()
{
var hostName = Dns.GetHostName();
Console.WriteLine($"hostname = {hostName}");
var val = System.Text.Encoding.UTF8.GetBytes(hostName).Sum(x => x);
// 取余
return val % (int)MaxDatacenterId;
}
生成器的構造函數
public IdGenerator(long datacenterId = -1)
{
if (datacenterId == -1)
{
// default
datacenterId = GetDatacenterId();
}
if (datacenterId > MaxDatacenterId || datacenterId < 0)
{
throw new ArgumentException("非法數據標志ID", nameof(datacenterId));
}
// 先檢驗再賦值
WorkerId = GetWorkerId();
DatacenterId = datacenterId;
Console.WriteLine($"w = {WorkerId}");
Console.WriteLine($"d = {DatacenterId}");
}
這里的數據中心可以讓使用方自己定義,默認-1的話,會根據HostName去生成一個。
這里給一個自定義的可選標識,主要還是考慮到了單機多實例,即一個IP上面部署多個實例。
雖然這個時候還是要考慮人工配置,不過已經從多機變成單機了,也算是一點簡化。畢竟大部分情況下也不會建議在同一個機器部署多個一樣的項目。
默認情況下的使用,IdGenerator對象要全局唯一,做成單例即可。
IdGenerator generator = new IdGenerator();
Parallel.For(0, 20, x =>
{
Console.WriteLine(generator.NextId());
});
Console.WriteLine("Hello World!");
System.Threading.Thread.Sleep(1000 * 60);
下面運行多個容器來模擬。
可以看到機器Id和數據中心Id都是沒有重復的。
在運行一次。
也是同樣的。
不足與展望
目前這種做法在應用實例少,機器數量少的情況下是基本可以滿足使用要求的了。老黃公司目前也就不到30台服務器,所以怎么都是夠用的。
但是依靠IP和HostName,隨着實例或機器的數量增多,沒有辦法保證它們取余算出來的一定是唯一的。
在這種情況下就需要考慮引用第三方存儲(Redis或數據庫)來保證這個的唯一性了。
下面是本文的示例代碼: