ASP.NET Core 產生連續 Guid

2022-07-31 15:00:26

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? http://guid.one/guid
    // see: https://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 https://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? http://guid.one/guid
        // see: https://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?

大端、小端基礎知識