本文不會過多解釋 Guid 是什麼,以及順序 Guid 的作用,需要讀者自行具備:
Guid.NetGuid()
產生的 Guid 是混亂無序的,想要一種產生順序 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?。
先大概講解 ABP 產生連續 Guid 的原始碼,並提出其問題(高並行產生的 Guid 並不連續)。
接著就問題,以及 ABP 的原始碼提出解決方案,並給出修改後的原始碼。
並會就 Sql Server 資料庫特殊的 Guid 排序方式,提出一種簡單的處理方案,讓 Sql Server 與 MySql 等資料庫保持一致的排序。
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 中的排布)。筆者也是看得一臉懵逼,就不在這裡誤人子弟了。
至於大端、小端,屬於計算機組成原理的知識,如果不記得了,可以自行百度(或參考大端、小端基礎知識)。
由於筆者只用過 MySql 和 Sql Server,測試也只用了這兩種資料庫測試,故而也只講這兩種。
richardtallent/RT.Comb這個倉庫也介紹了這一部分內容。
筆者的 MySql 版本為 8.0.26.
MySql 對 Guid 的處理為字串方式,排序方式為從左到右的。
故而決定順序的時間戳部分應該位於 Guid 的左側,所以 ABP 的原始碼裡 Guid 的16位元陣列:前6位為時間戳,後10位為亂數。
筆者的 Sql Server 版本為 2019 Express.
Sql Server 關於 Guid 的排序方式比較特殊,屬於分塊排序。
先排 Data4
的後6個位元組(即最後一塊,也即從第10個位元組開始的最後6個位元組),塊內依舊是從左到右排序。
接著排 Data4
的前2個位元組(即倒數第2塊,也即從第8個位元組開始的2個位元組),塊內依舊是從左到右排序。
隨後依次是 Data3
, Data2
, Data1
(其中,筆者驗證了 Data3
的塊內排序,並非從左到右,而是先排了塊內第2個位元組,後排第1個位元組,可能是 Sql Server 認為 Data3
是 Int16
,而小端處理後將2個位元組翻轉了,顯示雖然顯示了 Mxxx
,但實際上是 xxMx
,排序也是按後者來排).
故而決定順序的時間戳部分應該位於 Guid 的右側,所以 ABP 的原始碼裡 Guid 的16位元陣列:前10位為亂數,後6位為時間戳。
由於決定排序因素的部分為時間戳,而時間戳被處理成毫秒級。高並行的情況下,時間戳部分基本上一致,導致短時間內生成的 Guid 並不連續,是無序的。
// 獲取 Ticks,並處理為毫秒級(1個Tick為100ns,1ms=1000us=1000000ns)
long timestamp = DateTime.UtcNow.Ticks / 10000L;
這裡還是大概介紹一下 RFC 4122 版本4的內容:
Guid 組成形如:
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
其中 M 為 RFC 版本(version),版本4的話,值為4。
N 為變體(variant),值為 8, 9, A, B 其中一個。
版本4為保留版本號和變體,其他位均為隨機。
顯然,ABP 的方案,一部分是時間戳,餘下的部分均為亂數,這樣並不包含版本和變體,不屬於任何一版本的 Guid,為非標準的 Guid。
基於上述的方案的問題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);
}
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
筆者對於這一塊內容,也是一臉懵逼。
大概的思路是:在 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);
}
基於 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)");
}
}
}
這裡將完整的 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);
}
}
可以參考:Adnc 專案的文章:如何動態分配雪花演演算法的WorkerId.
Adnc 這個專案是風口旁的豬的,一個輕量級的微服務/分散式開發框架。
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 沒有太大區別,參考文章很齊全,可以看一看,這裡不一一列出)