乘風破浪,遇見最佳跨平臺跨終端框架.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

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章