前言
在數據庫設計中,我們常使用short
、int
、long
、Guid
的類型作爲主鍵。
其中short
、int
一般使用自動遞增的方式由數據庫生成,在EFCore中,它將會自動被設置成計算屬性,並在添加數據時自動計算生成([DatabaseGenerated(DatabaseGeneratedOption.Identity)]
)。
而實際系統中,我們使用long
和Guid
作爲主鍵類型更常見一些。
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
生成的值將是跨表跨庫都全局唯一的,無需擔心其重複問題,使用起來簡單方便,但確實佔用空間較大(16
byte),且因其無序性在排序和比較中性能相比整形類型慢。
Long和雪花算法
使用long
類型的主鍵可以帶來更好的性能和有序性,但是需要更加完善的機制來保障生成的值的唯一性,行業裏一個典型的實現包括由Twitter
公司早期開源的雪花算法(Snowflake
),由雪花算法生成的主鍵值具有全局唯一性和單調遞增性,在查詢數據時可用它來作爲排序字段,在寫入數據時將具有更高的索引性能。
MYSQL
的InnoDB
存儲引擎使用B+
樹存儲索引數據,主鍵數據一般也被設計爲索引,索引數據在B+
樹中是有序排列的,磁盤在插入數據時,先要尋道寫入的位置,採用雪花算法生成的值是有序的,所以在寫入索引時效率更高。
雪花算法採用64位的二進制表示,一共包括四個組成部分:
1
位是符號位,也就是最高位,始終是0
,沒有任何意義,因爲要是唯一計算機二進制補碼中就是負數,0
纔是正數。41
位是時間戳,具體到毫秒,41位的二進制可以使用69
年,因爲時間理論上永恆遞增,所以根據這個排序是可以的。10
位是機器標識,可以全部用作機器ID,也可以用來標識機房ID
+機器ID
,10位最多可以表示1024
臺機器。12
位是計數序列號,也就是同一臺機器上同一時間,理論上還可以同時生成不同的ID,12
位的序列號能夠區分出4096
個ID。
其中41
位時間戳可以使用時間的相對值,從相對值開始可以使用69
年,可設置符合我們實際需要的基準時間(Twepoch
=815818088000L
),讓使用壽命符合項目需要,一般採用當前時間戳(Timestamp
=(long)(DateTime.UtcNow - Jan1st1970).TotalMilliseconds
)減去相對時間得到的時間戳來參與運算。
可以使用時間和時間戳(毫秒)的轉換工具得到你想要的基準時間
這裏有個有意思的事情,你會發現,基準時間設置得離當前時間越近,最終得到的雪花值越短,所以要記得雪花值可不是固定長度,它會隨着時間推移,越來越長。
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();
}
運行效果
參考
- 關於全局ID,雪花(snowflake)算法的說明
- https://github.com/twitter/snowflake
- https://github.com/ccollie/snowflake-net
- https://github.com/dunitian/snowflake-net
- https://github.com/stulzq/snowflake-net
- 關於並發表唯一Id的問題,各位有什麼好方法。都進來看看啊……
- 閒談系列之一——數據庫主鍵GUID
- MYSQL中GUID和自增列做主鍵的優缺點
- 雪花算法(附工具類)
- .NET Core 使用HMAC算法
- 比雪花算法更好用的ID生成算法(單機或分佈式唯一ID)
- 面試官:講講雪花算法,越詳細越好
- 面試題:雪花算法(SnowFlake)如何解決時鐘回撥問題
- 時間戳轉換