背景
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或数据库)来保证这个的唯一性了。
下面是本文的示例代码: