乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 主键生成设计,论GUID/UUID和Long优劣,雪花算法原理、实现、驱动实体
前言
在数据库设计中,我们常使用short
、int
、long
、Guid
的类型作为主键。
其中short
、int
一般使用自动递增的方式由数据库生成,在EFCore中,它将会自动被设置成计算属性,并在添加数据时自动计算生成([DatabaseGenerated(DatabaseGeneratedOption.Identity)]
)。
而实际系统中,我们使用long
和Guid
作为主键类型更常见一些。
GUID和UUID
其中GUID
,全称Microsoft's Globally Unique Identifiers
是微软对通用唯一识别码(Universally Unique Identifier
, 简称UUID
)的一种实现。UUID
是一个软件建构的标准,是开源软件基金会(Open Software Foundation
, 简称OSF
)在分布式计算环境(Distributed Computing Environment
, 简称DCE
)领域的一部分,旨在让分布式系统所有元素都能具有唯一的辨识标记,而不需要中央控制端来实现辨识标记的生成。
GUID
的格式:xxxxxxxx-xxxx-xxxx-xxxxxx-xxxxxxxxxx
(8-4-4-4-12)UUID
的格式:xxxxxxxx-xxxx- xxxx-xxxxxxxxxxxxxxxx
(8-4-4-16)
使用GUID
生成的值将是跨表跨库都全局唯一的,无需担心其重复问题,使用起来简单方便,但确实占用空间较大(16
byte),且因其无序性在排序和比较中性能相比整形类型慢。
Long和雪花算法
使用long
类型的主键可以带来更好的性能和有序性,但是需要更加完善的机制来保障生成的值的唯一性,行业里一个典型的实现包括由Twitter
公司早期开源的雪花算法(Snowflake
),由雪花算法生成的主键值具有全局唯一性和单调递增性,在查询数据时可用它来作为排序字段,在写入数据时将具有更高的索引性能。
MYSQL
的InnoDB
存储引擎使用B+
树存储索引数据,主键数据一般也被设计为索引,索引数据在B+
树中是有序排列的,磁盘在插入数据时,先要寻道写入的位置,采用雪花算法生成的值是有序的,所以在写入索引时效率更高。
雪花算法采用64位的二进制表示,一共包括四个组成部分:
1
位是符号位,也就是最高位,始终是0
,没有任何意义,因为要是唯一计算机二进制补码中就是负数,0
才是正数。41
位是时间戳,具体到毫秒,41位的二进制可以使用69
年,因为时间理论上永恒递增,所以根据这个排序是可以的。10
位是机器标识,可以全部用作机器ID,也可以用来标识机房ID
+机器ID
,10位最多可以表示1024
台机器。12
位是计数序列号,也就是同一台机器上同一时间,理论上还可以同时生成不同的ID,12
位的序列号能够区分出4096
个ID。
其中41
位时间戳可以使用时间的相对值,从相对值开始可以使用69
年,可设置符合我们实际需要的基准时间(Twepoch
=815818088000L
),让使用寿命符合项目需要,一般采用当前时间戳(Timestamp
=(long)(DateTime.UtcNow - Jan1st1970).TotalMilliseconds
)减去相对时间得到的时间戳来参与运算。
可以使用时间和时间戳(毫秒)的转换工具得到你想要的基准时间
这里有个有意思的事情,你会发现,基准时间设置得离当前时间越近,最终得到的雪花值越短,所以要记得雪花值可不是固定长度,它会随着时间推移,越来越长。
2022-11-13 21:14:50
转化得到时间戳1668345290000L
得到的值是353789542400
2018-11-04 09:42:54
转化得到时间戳1541295774000L
得到的值是532886628746657792
2010-11-04 09:42:54
转化得到时间戳1288834974657L
得到的值是1591783021403439104
2000-00-00 00:00:00
转化得到时间戳943891200000L
得到的值是3038584380600614912
1995-11-08 16:08:08
转化得到时间戳815818088000L
得到的值是3575759299994976256
1980-12-08 17:08:08
转化得到时间戳345114488000L
得到的值是5550034286414921728
为了提高生成算法的计算速度,一般使用位运算(|
)和位移操作(<<
)。
其中10
位机器标识,可以按机房ID位数(DataCenterIdBits
=5
)和机器ID位数(WorkerIdBits
=5
)来拆分,那么可以支持32
个机房,每个机房支持32
台机器,一共可以支持1024
台机器。所以单机房最大机器ID数(MaxWorkerId
=-1L ^ -1L << WorkerIdBits
)、最大机房ID数(MaxDataCenterId
=-1L ^ -1L << DataCenterIdBits
)就可以计算得出。
其中12
位计数序列号,序列号位数(SequenceBits
=12
),通过运算可以得出序列号最大值(SequenceMask
=-1L ^ -1L << SequenceBits
),当当前时间戳和上一次时间戳相等的时候,我们就可以通过序列号(_sequence
=_sequence + 1 & SequenceMask
)自增来实现,如果超过最大值,就需要等待下一个毫秒时间区间。
其中机器ID偏左移12位(WorkerIdLeftShift
=SequenceBits
)、机房ID偏左移17位(DataCenterIdLeftShift
=SequenceBits + WorkerIdBits
)、时间戳左移22位(TimestampLeftShift
=SequenceBits + WorkerIdBits + DataCenterIdBits
),这个将会在生成中决定数据的位移位数。
雪花算法配置选项
依赖包
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Configuration.Binder
dotnet add package Microsoft.Extensions.Options
其中
SnowflakeOptions
定义为
/// <summary>
/// 雪花算法配置选项
/// </summary>
public class SnowflakeOptions
{
/// <summary>
/// 机器ID
/// </summary>
public int WorkerId { get; set; }
/// <summary>
/// 机房ID
/// </summary>
public int DataCenterId { get; set; }
}
对应的
appsettings.json
配置
{
"Snowflake": {
"WorkerId": 1,
"DataCenterId": 1
}
}
绑定配置
services.AddOptions<SnowflakeOptions>().Configure(options =>
{
configurationRoot.GetSection("Snowflake").Bind(options);
});
定义接口和算法实现
其中生成器接口
IGenerateProvider
定义为
/// <summary>
/// 生成器接口
/// </summary>
public interface IGenerateProvider
{
/// <summary>
/// 生成Id
/// </summary>
/// <returns></returns>
long GenerateId();
}
其中雪花算法生成器
SnowflakeGenerateProvider
实现为
/// <summary>
/// 雪花算法生成器
/// </summary>
public class SnowflakeGenerateProvider : IGenerateProvider
{
// 基准时间
const long Twepoch = 943891200000L;
// 机器ID位数
const int WorkerIdBits = 5;
// 机房ID位数
const int DataCenterIdBits = 5;
// 序列号位数
const int SequenceBits = 12;
// 机器ID单机房最小值
const int MinWorkerId = 0;
// 机器ID单机房最大值
const long MaxWorkerId = -1L ^ -1L << WorkerIdBits;
// 机房ID最小值
const int MinDataCenterId = 0;
// 机房ID最大值
const long MaxDataCenterId = -1L ^ -1L << DataCenterIdBits;
// 序列号ID最大值
const long SequenceMask = -1L ^ -1L << SequenceBits;
// 机器ID偏左移12位
private const int WorkerIdLeftShift = SequenceBits;
// 机房ID偏左移17位
private const int DataCenterIdLeftShift = SequenceBits + WorkerIdBits;
// 时间戳左移22位
public const int TimestampLeftShift = SequenceBits + WorkerIdBits + DataCenterIdBits;
/// <summary>
/// 当前序列号
/// </summary>
private long Sequence { get; set; } = 0L;
/// <summary>
/// 上一次时间戳
/// </summary>
private long LastTimestamp = -1L;
/// <summary>
/// 当前机器ID
/// </summary>
private readonly long WorkerId = 1L;
/// <summary>
/// 当前机房ID
/// </summary>
private readonly long DataCenterId = 1L;
/// <summary>
/// 生成锁
/// </summary>
private readonly object _generateLock = new object();
/// <summary>
/// 构造函数
/// </summary>
/// <param name="options"></param>
/// <exception cref="ArgumentException"></exception>
public SnowflakeGenerateProvider(IOptions<SnowflakeOptions> options)
{
WorkerId = options.Value.WorkerId;
if (WorkerId < MinWorkerId || WorkerId > MaxWorkerId)
{
throw new ArgumentException(string.Format("机器ID不得小于{0}且不得大于{1}", MinWorkerId, MaxWorkerId));
}
DataCenterId = options.Value.DataCenterId;
if (DataCenterId < MinDataCenterId || DataCenterId > MaxDataCenterId)
{
throw new ArgumentException(string.Format("机房ID不得小于{0}且不得大于{1}", MinDataCenterId, MaxDataCenterId));
}
}
/// <summary>
/// 生成Id
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public long GenerateId()
{
lock (_generateLock)
{
// 获取当前时间戳
var timestamp = GetCurrentTimestamp();
if (timestamp < LastTimestamp)
{
throw new ArgumentException(string.Format("当前时间戳必须大于上一次时间戳,已拒绝为{0}毫秒生成雪花ID", LastTimestamp - timestamp));
}
// 如果上一次时间戳和当前时间戳相等(同一个毫秒内)
if (LastTimestamp == timestamp)
{
// 启用序列号自增机制,并且和序列号最大值相与,去掉高位
Sequence = Sequence + 1 & SequenceMask;
// 如果自增已经超出了序列号最大值,就进入下一个毫秒循环
if (Sequence == 0)
{
// 等待下一个毫秒
timestamp = UntilNextTimestamp(LastTimestamp);
}
}
else
{
// 获取起始序列号
Sequence = GetDefaultSequence();
}
LastTimestamp = timestamp;
return timestamp - Twepoch << TimestampLeftShift | DataCenterId << DataCenterIdLeftShift | WorkerId << WorkerIdLeftShift | Sequence;
}
}
/// <summary>
/// 获取起始序列号
/// </summary>
/// <returns></returns>
private long GetDefaultSequence()
{
// 正常应该从0L开始,但是这里做个随机数,增加随机性
return new Random().Next(10);
}
/// <summary>
/// 等待下一个毫秒
/// </summary>
/// <param name="lastTimestamp"></param>
/// <returns></returns>
private long UntilNextTimestamp(long lastTimestamp)
{
var timestamp = GetCurrentTimestamp();
// 防止之前时间比当前时间更小
while (timestamp <= lastTimestamp)
{
timestamp = GetCurrentTimestamp();
}
return timestamp;
}
/// <summary>
/// 获取当前时间戳
/// </summary>
/// <returns></returns>
private long GetCurrentTimestamp()
{
DateTime Jan1st1970 = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
return (long)(DateTime.UtcNow - Jan1st1970).TotalMilliseconds;
}
}
简单使用示例
从DI容器中把它取出来,调用
GenerateId
方法即可。
using (var scope = services.BuildServiceProvider().CreateScope())
{
var generateProvider = scope.ServiceProvider.GetService<IGenerateProvider>();
System.Console.WriteLine($"SnowflakeId: {generateProvider.GenerateId()}");
}
输出结果
SnowflakeId: 3038602050964426754
与实体模型的结合
针对实体模型的扩展方法
为了降低业务入侵性,这里我们设计一个ID获取扩展方法IdGetterExtension
,其定义为
/// <summary>
/// ID获取扩展方法
/// </summary>
internal static class IdGetterExtension
{
/// <summary>
/// Key:Id类型
/// </summary>
public static readonly Dictionary<Type, Func<object>> IdFuncsForType
= new Dictionary<Type, Func<object>>();
/// <summary>
/// Key:Entity类型
/// </summary>
public static readonly Dictionary<Type, Func<object>> EntitiesIdFunc
= new Dictionary<Type, Func<object>>();
static IdGetterExtension()
{
IdFuncsForType[typeof(Guid)] = () => Guid.NewGuid();
}
public static TKey CreateIndentity<TKey>(this IEntity entity)
{
var entityType = entity.GetType();
if (EntitiesIdFunc.ContainsKey(entityType))
return (TKey)EntitiesIdFunc[entityType].Invoke();
var keyType = typeof(TKey);
if (IdFuncsForType.ContainsKey(keyType))
return (TKey)IdFuncsForType[keyType].Invoke();
else
return default;
}
}
这里利用委托的优势,建立了一个实体类型、实体主键类型和ID生成方法之间的映射表。
注意:这里在构造函数中,已经默认设置了
Guid
类型主键的生成方法,无需再额外设置了。
针对ID获取方法的扩展方法
为了更加方便的在外部将ID生成方法配置进去,这里定义了注册ID扩展方法RegisterIdExtension
,其定义为
/// <summary>
/// 注册ID扩展方法
/// </summary>
public static class RegisterIdExtension
{
/// <summary>
/// 注册指定类型ID生成方法
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <param name="func"></param>
public static void RegisterIdFunc<TKey>(Func<TKey> func)
{
IdGetterExtension.IdFuncsForType[typeof(TKey)] = () => func.Invoke();
}
/// <summary>
/// 注册注定实体ID生成方法
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TEntity"></typeparam>
/// <param name="func"></param>
public static void RegisterIdFunc<TKey, TEntity>(Func<TKey> func)
where TEntity : Entity<TKey>
{
IdGetterExtension.EntitiesIdFunc[typeof(TEntity)] = () => func.Invoke();
}
}
将需要扩展的ID生成方法添加到方法表中
这里我们只需要把前面的生成器接口中的生成ID方法从DI容器中取出来,然后通过这个扩展方法添加进去即可。
RegisterIdExtension.RegisterIdFunc(() => serviceProvider.GetService<IGenerateProvider>().GenerateId());
在实体模型的构造函数中调用ID生成扩展方法
接下来很简单,只需要在实体模型基类中的构造函数调用生成主键的方法即可。
/// <summary>
/// 实体抽象类(泛型)
/// </summary>
/// <typeparam name="TKey"></typeparam>
public abstract class Entity<TKey> : Entity, IEntity<TKey>
{
protected Entity()
{
Id = this.CreateIndentity<TKey>();
}
}
创建实体示例
using (var scope = services.BuildServiceProvider().CreateScope())
{
var context = scope.ServiceProvider.GetService<PractingContext>();
var blog = new Blog("https://www.cnblogs.com/taylorshi/p/16886409.html");
context.Blogs.Add(blog);
await context.SaveChangesAsync();
}
运行效果
参考
- 关于全局ID,雪花(snowflake)算法的说明
- https://github.com/twitter/snowflake
- https://github.com/ccollie/snowflake-net
- https://github.com/dunitian/snowflake-net
- https://github.com/stulzq/snowflake-net
- 关于并发表唯一Id的问题,各位有什么好方法。都进来看看啊……
- 闲谈系列之一——数据库主键GUID
- MYSQL中GUID和自增列做主键的优缺点
- 雪花算法(附工具类)
- .NET Core 使用HMAC算法
- 比雪花算法更好用的ID生成算法(单机或分布式唯一ID)
- 面试官:讲讲雪花算法,越详细越好
- 面试题:雪花算法(SnowFlake)如何解决时钟回拨问题
- 时间戳转换