乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 主键生成设计,论GUID/UUID和Long优劣,雪花算法原理、实现、驱动实体

前言

在数据库设计中,我们常使用shortintlongGuid的类型作为主键。

其中shortint一般使用自动递增的方式由数据库生成,在EFCore中,它将会自动被设置成计算属性,并在添加数据时自动计算生成([DatabaseGenerated(DatabaseGeneratedOption.Identity)])。

而实际系统中,我们使用longGuid作为主键类型更常见一些。

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生成的值将是跨表跨库都全局唯一的,无需担心其重复问题,使用起来简单方便,但确实占用空间较大(16byte),且因其无序性在排序和比较中性能相比整形类型慢。

Long和雪花算法

使用long类型的主键可以带来更好的性能和有序性,但是需要更加完善的机制来保障生成的值的唯一性,行业里一个典型的实现包括由Twitter公司早期开源的雪花算法(Snowflake),由雪花算法生成的主键值具有全局唯一性单调递增性,在查询数据时可用它来作为排序字段,在写入数据时将具有更高的索引性能。

image

MYSQLInnoDB存储引擎使用B+树存储索引数据,主键数据一般也被设计为索引,索引数据在B+树中是有序排列的,磁盘在插入数据时,先要寻道写入的位置,采用雪花算法生成的值是有序的,所以在写入索引时效率更高。

雪花算法采用64位的二进制表示,一共包括四个组成部分:

  • 1位是符号位,也就是最高位,始终是0,没有任何意义,因为要是唯一计算机二进制补码中就是负数,0才是正数。
  • 41位是时间戳,具体到毫秒,41位的二进制可以使用69年,因为时间理论上永恒递增,所以根据这个排序是可以的。
  • 10位是机器标识,可以全部用作机器ID,也可以用来标识机房ID + 机器ID,10位最多可以表示1024台机器。
  • 12位是计数序列号,也就是同一台机器上同一时间,理论上还可以同时生成不同的ID,12位的序列号能够区分出4096个ID。

image

其中41位时间戳可以使用时间的相对值,从相对值开始可以使用69年,可设置符合我们实际需要的基准时间(Twepoch=815818088000L),让使用寿命符合项目需要,一般采用当前时间戳(Timestamp=(long)(DateTime.UtcNow - Jan1st1970).TotalMilliseconds)减去相对时间得到的时间戳来参与运算。

可以使用时间和时间戳(毫秒)的转换工具得到你想要的基准时间

image

这里有个有意思的事情,你会发现,基准时间设置得离当前时间越近,最终得到的雪花值越短,所以要记得雪花值可不是固定长度,它会随着时间推移,越来越长。

  • 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();
}

运行效果

image

参考

posted @ 2022-11-13 17:37  TaylorShi  阅读(783)  评论(0编辑  收藏  举报