ASP.NET Core 產生連續 Guid

1 前言

1.1 這篇文章面向的讀者

本文不會過多解釋 Guid 是什麼,以及順序 Guid 的作用,需要讀者自行具備:

  • 知道 Guid,並且清楚其作用與優勢
  • 清楚 Guid.NetGuid() 產生的 Guid 是混亂無序的,想要一種產生順序 Guid 的算法來保證數據庫的高效運行

1.2 連續 Guid 的原理

Guid 形如:

08da7241-170b-c8ef-a094-06224a651c6a

該 Guid 有16位元組(byte)共128位(bit),可以包含時間戳,而順序 Guid 主要是根據時間戳順序來實現的,所以時間戳的部分,作為排序的決定性因素

如示例中,前8個位元組的內容為時間戳,將其轉為十進制為:

637947921111435500

這是一個以時鐘周期數(Tick)為單位的時間戳,為從公元1年1月1日0點至今的時鐘周期數,1個 Tick 為 100ns(參考微軟官方關於 Ticks 的介紹)。

註:上方示例的 Guid 並不符合 RFC 4122 標準,至於什麼是 RFC 4122 標準,以及 Guid 的版本,這裡不展開,讀者自行參考什麼是 GUID?

1.3 本文思路

先大概講解 ABP 產生連續 Guid 的源碼,並提出其問題(高並發產生的 Guid 並不連續)。

接着就問題,以及 ABP 的源碼提出解決方案,並給出修改後的源碼。

並會就 Sql Server 數據庫特殊的 Guid 排序方式,提出一種簡單的處理方案,讓 Sql Server 與 MySql 等數據庫保持一致的排序。

2 ABP 連續 Guid 的實現

2.1 ABP 連續 Guid 源碼

ABP產生連續 Guid 的源碼,來源於:jhtodd/SequentialGuid.

該方式,產生的 Guid 有6個位元組是時間戳(毫秒級),10個位元組是隨機數。

其中,順序 Guid 主要是根據時間戳順序來實現的,所以時間戳的部分,作為排序的決定性因素。

源碼主要的部分摘錄如下:

public class SequentialGuidGenerator : IGuidGenerator, ITransientDependency
{
    public Guid Create(SequentialGuidType guidType)
    {
        // 獲取 10 位元組隨機序列數組
        var randomBytes = new byte[10];
        RandomNumberGenerator.GetBytes(randomBytes);

        // 獲取 Ticks,並處理為毫秒級(1個Tick為100ns,1ms=1000us=1000000ns)
        long timestamp = DateTime.UtcNow.Ticks / 10000L;

        // 時間戳轉為 byte 數組
        byte[] timestampBytes = BitConverter.GetBytes(timestamp);

        // 因為數組是從 int64 轉化過來的,如果是在小端系統中(little-endian),需要翻轉
        if (BitConverter.IsLittleEndian)
        {
            Array.Reverse(timestampBytes);
        }

        byte[] guidBytes = new byte[16];

        switch (guidType)
        {
            case SequentialGuidType.SequentialAsString:
            case SequentialGuidType.SequentialAsBinary:
                
                // 16位數組:前6位為時間戳,後10位為隨機數
                Buffer.BlockCopy(timestampBytes, 2, guidBytes, 0, 6);
                Buffer.BlockCopy(randomBytes, 0, guidBytes, 6, 10);
                
                // .NET中,Data1 和 Data2塊 分別視為 Int32 和 Int16
                // 跟時間戳從 Int64 轉 byte 數組後需要翻轉一個理,在小端系統,需要翻轉這兩個塊。
                if (guidType == SequentialGuidType.SequentialAsString && BitConverter.IsLittleEndian)
                {
                    Array.Reverse(guidBytes, 0, 4);
                    Array.Reverse(guidBytes, 4, 2);
                }
                break;
                
            case SequentialGuidType.SequentialAtEnd:
                
                // 16位數組:前10位為隨機數,後6位為時間戳
                Buffer.BlockCopy(randomBytes, 0, guidBytes, 0, 10);
                Buffer.BlockCopy(timestampBytes, 2, guidBytes, 10, 6);
                break;
        }
        return new Guid(guidBytes);
    }
}

RandomNumberGenerator 用於生成隨機序列數組。

DateTime.UtcNow.Ticks 為獲取從公元1年1月1日0點至今的時鐘周期數,1個Tick為100ns(微軟官方關於 Ticks 的介紹)。

SequentialGuidType 為產生連續 Guid 的類別,默認為 SequentialAtEnd ,定義如下:

public enum SequentialGuidType
{
    /// <summary>
    /// 用於 MySql 和 PostgreSql.當使用 Guid.ToString() 方法進行格式化時連續.
    /// </summary>
    SequentialAsString,

    /// <summary>
    /// 用於 Oracle.當使用 Guid.ToByteArray() 方法進行格式化時連續.
    /// </summary>
    SequentialAsBinary,

    /// <summary>
    /// 用以 SqlServer.連續性體現於 GUID 的第4塊(Data4).
    /// </summary>
    SequentialAtEnd
}

如各個枚舉屬性的 summary 描述,主要是因為數據庫關於 Guid 排序方式的不同。

至於代碼中需要翻轉 byte 數組的部分,這一部分,可以參考:Is there a .NET equivalent to SQL Server’s newsequentialid()(Stack Overflow 這個問題,有一個回答介紹了時間戳高低位在 Guid 中的排布)。筆者也是看得一臉懵逼,就不在這裡誤人子弟了。

至於大端、小端,屬於計算機組成原理的知識,如果不記得了,可以自行百度(或參考大端、小端基礎知識)。

2.2 不同數據庫 Guid 的排序方式

由於筆者只用過 MySql 和 Sql Server,測試也只用了這兩種數據庫測試,故而也只講這兩種。

richardtallent/RT.Comb這個倉庫也介紹了這一部分內容。

(1)MySql

筆者的 MySql 版本為 8.0.26.

MySql 對 Guid 的處理為字符串方式,排序方式為從左到右的。

故而決定順序的時間戳部分應該位於 Guid 的左側,所以 ABP 的源碼里 Guid 的16位數組:前6位為時間戳,後10位為隨機數。

(2)Sql Server

筆者的 Sql Server 版本為 2019 Express.

Sql Server 關於 Guid 的排序方式比較特殊,屬於分塊排序。

先排 Data4 的後6個位元組(即最後一塊,也即從第10個位元組開始的最後6個位元組),塊內依舊是從左到右排序。

接着排 Data4 的前2個位元組(即倒數第2塊,也即從第8個位元組開始的2個位元組),塊內依舊是從左到右排序。

隨後依次是 Data3, Data2, Data1 (其中,筆者驗證了 Data3 的塊內排序,並非從左到右,而是先排了塊內第2個位元組,後排第1個位元組,可能是 Sql Server 認為 Data3Int16,而小端處理後將2個位元組翻轉了,顯示雖然顯示了 Mxxx,但實際上是 xxMx,排序也是按後者來排).

故而決定順序的時間戳部分應該位於 Guid 的右側,所以 ABP 的源碼里 Guid 的16位數組:前10位為隨機數,後6位為時間戳。

2.3 存在的問題

(1)毫秒級的時間戳

由於決定排序因素的部分為時間戳,而時間戳被處理成毫秒級。高並發的情況下,時間戳部分基本上一致,導致短時間內生成的 Guid 並不連續,是無序的。

// 獲取 Ticks,並處理為毫秒級(1個Tick為100ns,1ms=1000us=1000000ns)
long timestamp = DateTime.UtcNow.Ticks / 10000L;

(2)非標準 Guid

這裡還是大概介紹一下 RFC 4122 版本4的內容:

Guid 組成形如:

xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx

其中 M 為 RFC 版本(version),版本4的話,值為4。

N 為變體(variant),值為 8, 9, A, B 其中一個。

版本4為保留版本號和變體,其他位均為隨機。

顯然,ABP 的方案,一部分是時間戳,餘下的部分均為隨機數,這樣並不包含版本和變體,不屬於任何一版本的 Guid,為非標準的 Guid。

3 連續 Guid 修改版本

3.1 解決高並發時間戳一致問題

(1)實現

基於上述的方案的問題1,由於問題是高並發的情況下時間戳一致的問題,那麼盡量讓時間戳的間隔再小一點,即可,如修改時間戳的代碼為:

long timestamp = DateTime.UtcNow.Ticks;

直接將毫秒的處理去掉,讓時間戳為納秒級(ns)。

另外,還需要將時間戳原本只取6個位元組,改成8個位元組,讓尾部的時間戳作用於 Guid 上。

完整的代碼修改如下:

public static Guid Next(SequentialGuidType guidType)
{
    // 原先 10 位元組的隨機序列數組,減少為 8 位元組
    var randomBytes = new byte[8];
    _randomNumberGenerator.GetBytes(randomBytes);

    // 時間戳保持納秒級,不額外處理
    long timestamp = DateTime.UtcNow.Ticks;

    byte[] timestampBytes = BitConverter.GetBytes(timestamp);
    if (BitConverter.IsLittleEndian)
    {
        Array.Reverse(timestampBytes);
    }

    byte[] guidBytes = new byte[16];

    switch (guidType)
    {
        case SequentialGuidType.SequentialAsString:
        case SequentialGuidType.SequentialAsBinary:

            // 16位數組:前8位為時間戳,後8位為隨機數
            Buffer.BlockCopy(timestampBytes, 0, guidBytes, 0, 8);
            Buffer.BlockCopy(randomBytes, 0, guidBytes, 8, 8);

            // .NET中,Data1、Data2、Data3 塊 分別視為 Int32、Int16、Int16
            // 跟時間戳從 Int64 轉 byte 數組後需要翻轉一個理,在小端系統,需要翻轉這3個塊。
            if (guidType == SequentialGuidType.AsString && BitConverter.IsLittleEndian)
            {
                Array.Reverse(guidBytes, 0, 4);
                Array.Reverse(guidBytes, 4, 2);
                Array.Reverse(guidBytes, 6, 2); // 翻轉
            }

            break;

        case SequentialGuidType.SequentialAtEnd:

            // 16位數組:前8位為隨機數,後8位為時間戳
            Buffer.BlockCopy(randomBytes, 0, guidBytes, 0, 8);
            // 方案1:正常拼接。這種方式只能連續1年+
            Buffer.BlockCopy(timestampBytes, 0, guidBytes, 8, 8);
            // 方案2:將時間戳末尾的2個位元組,放到 Data4 的前2個位元組
            Buffer.BlockCopy(timestampBytes, 6, guidBytes, 8, 2);
            Buffer.BlockCopy(timestampBytes, 0, guidBytes, 10, 6);
            break;
    }

    return new Guid(guidBytes);
}

(2)測試

AsString 方式:

# 主要影響排序的,體現在 Guid 第8個位元組。
08da7241-170b-c8ef-a094-06224a651c6a	0
08da7241-170b-d141-6ffc-5cdcecec5db9	1
08da7241-170b-d14e-d49e-81ce5efa6143	2
08da7241-170b-d150-8f59-836eab8d1939	3
08da7241-170b-d152-ac41-0c357a8aa4a1	4
08da7241-170b-d163-90a4-6083d462eeaf	5
08da7241-170b-d175-25b2-1d47ddd25939	6
08da7241-170b-d178-aa93-dc86e6391438	7
08da7241-170b-d185-619f-c24faf992806	8
08da7241-170b-d188-bd51-e36029ad9816	9

AtEnd 方式:

// 順序體現在最後一個位元組
983C1A57-8C2B-DE7D-08DA-724214AED77D	0
4F1389B8-59F6-7C78-08DA-724214AEDAB6	1
CF6D52B1-3BFA-272F-08DA-724214AEDABC	2
017C4F99-4499-67DB-08DA-724214AEDABE	3
4B0A0685-4355-2060-08DA-724214AEDAC0	4
D690E344-DDB4-16CB-08DA-724214AEDAC6	5
6E22CDBE-65FE-64DC-08DA-724214AEDAC8	6
72E67EB4-CA92-DF3A-08DA-724214AEDACA	7
AA93D914-5415-21C9-08DA-724214AEDACB	8
9D93FA3F-84B6-519D-08DA-724214AEDACD	9

3.2 產生符合 RFC 4122 標準的 Guid

筆者對於這一塊內容,也是一臉懵逼。

大概的思路是:在 ABP 連續 Guid 的方案中,插入版本(M)和變體(N),那麼犧牲1個位元組(byte)共8個位(bit)的隨機數即可,影響到時間戳的部分,則往後挪一挪。

xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx

修改後的代碼比較複雜,如下:

public static Guid Next(SequentialGuidType guidType)
{
    // see: What is a GUID? //guid.one/guid
    // see: //github.com/richardtallent/RT.Comb#gory-details-about-uuids-and-guids
    // According to RFC 4122:
    // dddddddd-dddd-Mddd-Ndrr-rrrrrrrrrrrr
    // - M = RFC 版本(version), 版本4的話,值為4
    // - N = RFC 變體(variant),值為 8, 9, A, B 其中一個,這裡固定為8
    // - d = 從公元1年1月1日0時至今的時鐘周期數(DateTime.UtcNow.Ticks)
    // - r = 隨機數(random bytes)

    var randomBytes = new byte[8];
    _randomNumberGenerator.GetBytes(randomBytes);

    byte version = (byte)4;
    byte variant = (byte)8;
    byte filterHighBit = 0b00001111;
    byte filterLowBit = 0b11110000;

    long timestamp = DateTime.UtcNow.Ticks;

    byte[] timestampBytes = BitConverter.GetBytes(timestamp);
    if (BitConverter.IsLittleEndian)
    {
        Array.Reverse(timestampBytes);
    }

    byte[] guidBytes = new byte[16];

    switch (guidType)
    {
        case SequentialGuidType.SequentialAsString:
        case SequentialGuidType.SequentialAsBinary:

            // AsString: dddddddd-dddd-Mddd-Ndrr-rrrrrrrrrrrr
            Buffer.BlockCopy(timestampBytes, 0, guidBytes, 0, 6); // 時間戳前6個位元組,共48位
            // guidBytes[6]:高4位為版本 | 低4位取時間戳序號[6]的元素的高4位
            guidBytes[6] = (byte)((version << 4) | ((timestampBytes[6] & filterLowBit) >> 4)); 
            // guidBytes[7]:高4位取:[6]低4位 | 低4位取:[7]高4位
            guidBytes[7] = (byte)(((timestampBytes[6] & filterHighBit) << 4) | ((timestampBytes[7] & filterLowBit) >> 4)); 
            // guidBytes[8]:高4位為:變體 | 低4位取:[7]低4位
            guidBytes[8] = (byte)((variant << 4) | (timestampBytes[7] & filterHighBit)); 
            Buffer.BlockCopy(randomBytes, 0, guidBytes, 9, 7); // 餘下7個位元組由隨機數組填充

            // .NET中,Data1、Data2、Data3 塊 分別視為 Int32、Int16、Int16,在小端系統,需要翻轉這3個塊。
            if (guidType == SequentialGuidType.AsString && BitConverter.IsLittleEndian)
            {
                Array.Reverse(guidBytes, 0, 4);
                Array.Reverse(guidBytes, 4, 2);
                Array.Reverse(guidBytes, 6, 2);
            }

            break;

        case SequentialGuidType.SequentialAtEnd:

            // AtEnd: rrrrrrrr-rrrr-Mxdr-Nddd-dddddddddddd
            // Block: 1        2    3    4    5
            // Data4 = Block4 + Block5
            // 排序順序:Block5 > Block4 > Block3 > Block2 > Block1
            // Data3 = Block3 被認為是 uint16,排序並不是從左到右,為消除影響,x 位取固定值
            
            Buffer.BlockCopy(randomBytes, 0, guidBytes, 0, 6);
            // Mx 高4位為版本 | 低4位取:全0
            guidBytes[6] = (byte)(version << 4); 
            // dr 高4位為:時間戳[7]低4位 | 低4位取:隨機數
            guidBytes[7] = (byte)(((timestampBytes[7] & filterHighBit) << 4) | (randomBytes[7] & filterHighBit)); 
            // Nd 高4位為:變體 | 低4位取:時間戳[6]高4位
            guidBytes[8] = (byte)((variant << 4) | ((timestampBytes[6] & filterLowBit) >> 4)); 
            // dd 高4位為:時間戳[6]低4位 | 低4位取:時間戳[7]高4位
            guidBytes[9] = (byte)(((timestampBytes[6] & filterHighBit) << 4) | ((timestampBytes[7] & filterLowBit) >> 4)); 
            Buffer.BlockCopy(timestampBytes, 0, guidBytes, 10, 6); // 時間戳前6個位元組

            if (BitConverter.IsLittleEndian)
            {
                //Array.Reverse(guidBytes, 0, 4); // 隨機數就不翻轉了
                //Array.Reverse(guidBytes, 4, 2);
                Array.Reverse(guidBytes, 6, 2); // 包含版本號的 Data3 塊需要翻轉
            }
            break;
    }

    return new Guid(guidBytes);
}

4 Sql Server 關於 Guid 的處理方案

基於 Sql Server 特殊的 Guid 排序方式,這裡提出一種解決方案:

不使用 Sql Server 默認的 [uniqueidentifier] 而改用 char(36),這樣能讓 Sql Server 的 Guid 處理成字符串,令其排序方式與字符串一致(與 MySql 和 C# 程序中的排序統一)。

具體處理可以在自定義的 DbContext 的 OnModelCreating 中配置:

// 獲取所有註冊的實體,遍歷
foreach (var entityType in builder.Model.GetEntityTypes())
{
    // 獲取實體的所有屬性,遍歷
    PropertyInfo[] propertyInfos = entityType.ClrType.GetProperties();
    foreach (PropertyInfo propertyInfo in propertyInfos)
    {
        string propertyName = propertyInfo.Name;
        if (propertyInfo.PropertyType.FullName == "System.Guid")
        {
            // 將 Guid 類型設置為 char(36)
            builder.Entity(entityType.ClrType).Property(propertyName).HasColumnType("char(36)");
        }
    }
}

5 完整的代碼

這裡將完整的 GuidHelper 給出:

通過 GuidHelper.Next() 生成連續的 Guid.

using System.Security.Cryptography;

public enum SequentialGuidType
{
    /// <summary>
    /// 用於 MySql 和 PostgreSql.
    ///  當使用 <see cref="Guid.ToString()" /> 方法進行格式化時連續.
    /// </summary>
    AsString,

    /// <summary>
    /// 用於 Oracle.
    /// 當使用 <see cref="Guid.ToByteArray()" /> 方法進行格式化時連續.
    /// </summary>
    AsBinary,

    /// <summary>
    /// 用以 SqlServer.
    /// 連續性體現於 GUID 的第4塊(Data4).
    /// </summary>
    AtEnd
}

public static class GuidHelper
{
    private const byte version = (byte)4;
    private const byte variant = (byte)8;
    private const byte filterHighBit = 0b00001111;
    private const byte filterLowBit = 0b11110000;
    private static readonly RandomNumberGenerator _randomNumberGenerator = RandomNumberGenerator.Create();

    /// <summary>
    /// 連續 Guid 類型,默認:AsString.
    /// </summary>
    public static SequentialGuidType SequentialGuidType { get; set; } = SequentialGuidType.AsString;

    /// <summary>
    /// 生成連續 Guid.
    /// </summary>
    /// <returns></returns>
    public static Guid Next()
    {
        return Next(SequentialGuidType);
    }

    /// <summary>
    /// 生成連續 Guid(生成的 Guid 並不符合 RFC 4122 標準).
    /// 來源:Abp. from jhtodd/SequentialGuid //github.com/jhtodd/SequentialGuid/blob/master/SequentialGuid/Classes/SequentialGuid.cs .
    /// </summary>
    /// <param name="guidType"></param>
    /// <returns></returns>
    public static Guid NextOld(SequentialGuidType guidType)
    {
        var randomBytes = new byte[8];
        _randomNumberGenerator.GetBytes(randomBytes);

        long timestamp = DateTime.UtcNow.Ticks;

        byte[] timestampBytes = BitConverter.GetBytes(timestamp);
        if (BitConverter.IsLittleEndian)
        {
            Array.Reverse(timestampBytes);
        }

        byte[] guidBytes = new byte[16];

        switch (guidType)
        {
            case SequentialGuidType.AsString:
            case SequentialGuidType.AsBinary:

                // 16位數組:前8位為時間戳,後8位為隨機數
                Buffer.BlockCopy(timestampBytes, 0, guidBytes, 0, 8);
                Buffer.BlockCopy(randomBytes, 0, guidBytes, 8, 8);

                // .NET中,Data1、Data2、Data3 塊 分別視為 Int32、Int16、Int16,在小端系統,需要翻轉這3個塊。
                if (guidType == SequentialGuidType.AsString && BitConverter.IsLittleEndian)
                {
                    Array.Reverse(guidBytes, 0, 4);
                    Array.Reverse(guidBytes, 4, 2);
                    Array.Reverse(guidBytes, 6, 2);
                }

                break;

            case SequentialGuidType.AtEnd:

                // 16位數組:前8位為隨機數,後8位為時間戳
                Buffer.BlockCopy(randomBytes, 0, guidBytes, 0, 8);
                Buffer.BlockCopy(timestampBytes, 6, guidBytes, 8, 2);
                Buffer.BlockCopy(timestampBytes, 0, guidBytes, 10, 6);
                break;
        }

        return new Guid(guidBytes);
    }

    /// <summary>
    /// 生成連續 Guid.
    /// </summary>
    /// <param name="guidType"></param>
    /// <returns></returns>
    public static Guid Next(SequentialGuidType guidType)
    {
        // see: What is a GUID? //guid.one/guid
        // see: //github.com/richardtallent/RT.Comb#gory-details-about-uuids-and-guids
        // According to RFC 4122:
        // dddddddd-dddd-Mddd-Ndrr-rrrrrrrrrrrr
        // - M = RFC 版本(version), 版本4的話,值為4
        // - N = RFC 變體(variant),值為 8, 9, A, B 其中一個,這裡固定為8
        // - d = 從公元1年1月1日0時至今的時鐘周期數(DateTime.UtcNow.Ticks)
        // - r = 隨機數(random bytes)

        var randomBytes = new byte[8];
        _randomNumberGenerator.GetBytes(randomBytes);

        long timestamp = DateTime.UtcNow.Ticks;

        byte[] timestampBytes = BitConverter.GetBytes(timestamp);
        if (BitConverter.IsLittleEndian)
        {
            Array.Reverse(timestampBytes);
        }

        byte[] guidBytes = new byte[16];

        switch (guidType)
        {
            case SequentialGuidType.AsString:
            case SequentialGuidType.AsBinary:

                // AsString: dddddddd-dddd-Mddd-Ndrr-rrrrrrrrrrrr
                Buffer.BlockCopy(timestampBytes, 0, guidBytes, 0, 6); // 時間戳前6個位元組,共48位
                guidBytes[6] = (byte)((version << 4) | ((timestampBytes[6] & filterLowBit) >> 4)); // 高4位為版本 | 低4位取時間戳序號[6]的元素的高4位
                guidBytes[7] = (byte)(((timestampBytes[6] & filterHighBit) << 4) | ((timestampBytes[7] & filterLowBit) >> 4)); // 高4位取:[6]低4位 | 低4位取:[7]高4位
                guidBytes[8] = (byte)((variant << 4) | (timestampBytes[7] & filterHighBit)); // 高4位為:變體 | 低4位取:[7]低4位
                Buffer.BlockCopy(randomBytes, 0, guidBytes, 9, 7); // 餘下7個位元組由隨機數組填充

                // .NET中,Data1、Data2、Data3 塊 分別視為 Int32、Int16、Int16,在小端系統,需要翻轉這3個塊。
                if (guidType == SequentialGuidType.AsString && BitConverter.IsLittleEndian)
                {
                    Array.Reverse(guidBytes, 0, 4);
                    Array.Reverse(guidBytes, 4, 2);
                    Array.Reverse(guidBytes, 6, 2);
                }

                break;

            case SequentialGuidType.AtEnd:

                // AtEnd: rrrrrrrr-rrrr-Mxdr-Nddd-dddddddddddd
                // Block: 1        2    3    4    5
                // Data4 = Block4 + Block5
                // 排序順序:Block5 > Block4 > Block3 > Block2 > Block1
                // Data3 = Block3 被認為是 uint16,排序並不是從左到右,為消除影響,x 位取固定值
                
                Buffer.BlockCopy(randomBytes, 0, guidBytes, 0, 6);
                guidBytes[6] = (byte)(version << 4); // Mx 高4位為版本 | 低4位取:全0
                guidBytes[7] = (byte)(((timestampBytes[7] & filterHighBit) << 4) | (randomBytes[7] & filterHighBit)); ; // dr 高4位為:時間戳[7]低4位 | 低4位取:隨機數
                guidBytes[8] = (byte)((variant << 4) | ((timestampBytes[6] & filterLowBit) >> 4)); // Nd 高4位為:變體 | 低4位取:時間戳[6]高4位
                guidBytes[9] = (byte)(((timestampBytes[6] & filterHighBit) << 4) | ((timestampBytes[7] & filterLowBit) >> 4)); // dd 高4位為:時間戳[6]低4位 | 低4位取:時間戳[7]高4位
                Buffer.BlockCopy(timestampBytes, 0, guidBytes, 10, 6); // 時間戳前6個位元組

                if (BitConverter.IsLittleEndian)
                {
                    Array.Reverse(guidBytes, 6, 2); // 包含版本號的 Data3 塊需要翻轉
                }
                break;
        }

        return new Guid(guidBytes);
    }
}

6 其他全局唯一算法推薦

6.1 雪花算法

可以參考:Adnc 項目的文章:如何動態分配雪花算法的WorkerId.

Adnc 這個項目是風口旁的豬的,一個輕量級的微服務/分佈式開發框架。

參考來源

ABP產生連續 Guid 的源碼

DateTime.Ticks(微軟官方關於 Ticks 的介紹,1個 Ticks 是100ns)

Guid Generator is not sequential generating multiple call in one request(ABP 的 issue)

Is there a .NET equivalent to SQL Server’s newsequentialid()(Stack Overflow 這個問題,有一個回答介紹了時間戳高低位在 Guid 中的排布)

Pomelo.EntityFrameworkCore.MySql 連續 Guid 的源碼(Furion 源碼看到的,這個方案我看不懂,大概理解了一下,實際上原理應該差不多,生成的 Guid 的連續的字符串。不過,這裡生成的 Guid 是符合 Guid 的 RFC 4122 Version 4 標準的)

不同數據庫 Guid 的排序規則(講了 MSSQL 即 Sql Server,還有 PostgreSQL)

.NET生成多數據庫有序Guid(這篇貼出的源碼與 Abp 沒有太大區別,參考文章很齊全,可以看一看,這裡不一一列出)

UUID(GUID)不同版本和順序遞增探究

什麼是 GUID?

大端、小端基礎知識