.NET Core中反解ObjectId
前言
在設計資料庫的時候,我們通常需要給業務資料表分配主鍵,很多時候,為了省事,我都是直接使用 GUID/UUID 的方式,但是在 MonggoDB 中,其內部實現了 ObjectId(以下統稱為Oid)。並且在.NETCore 的驅動中給出了原始碼的實現。
經過仔細研讀官方的原始碼後發現,其實現原理非常的簡單易學,在最新的版本中,閹割了 UnPack 函式,可能是官方覺得解包是沒什麼太多的使用場景的,但是我們認為,對於資料溯源來說,解包的操作實在是非常有必要,特別是在目前的微服務大流行的背景下。
為此,在參考官方程式碼的基礎上進行了部分改進,增加了一下自己的需求。本示例程式碼增加了解包的操作、對 string 的隱式轉換、提供讀取解包後資料的公開屬性。
ObjectId 的資料結構
首先,我們來看 Oid 的資料結構的設計。
從上圖可以看出,Oid 的資料結構主要由四個部分組成,分別是:Unix時間戳、機器名稱、程序編號、自增編號。Oid 實際上是總長度為12個位元組24的字串,易記口訣為:4323,時間4位元組,機器名3位元組,程序編號2位元組,自增編號3位元組。
1、Unix時間戳:Unix時間戳以秒為記錄單位,即從1970/1/1 00:00:00 開始到當前時間的總秒數。
2、機器名稱:記錄當前生產Oid的裝置號
3、程序編號:當前執行Oid程式的編號
4、自增編號:在當前秒內,每次呼叫都將自動增長(已實現執行緒安全)
根據演算法可知,當前一秒內產生的最大 id 數量為 2^24=16777216 條記錄,所以無需過多擔心 id 碰撞的問題。
實現思路
先來看一下程式碼實現後的類結構圖。
通過上圖可以發現,類圖主要由兩部分組成,ObjectId/ObjectIdFactory,在類 ObjectId 中,主要實現了生產、解包、計算、轉換、公開資料結構等操作,而 ObjectIdFactory 只有一個功能,就是生產 Oid。
所以,我們知道,類 ObjectId 中的 NewId 實際是呼叫了 ObjectIdFactory 的 NewId 方法。
為了生產效率的問題,在 ObjectId 中聲明瞭靜態的 ObjectIdFactory 物件,有一些初始化的工作需要在程式啟動的時候在 ObjectIdFactory 的建構函式內部完成,比如獲取機器名稱和程序編號,這些都是一次性的工作。
類 ObjectIdFactory 的程式碼實現
public class ObjectIdFactory { private int increment; private readonly byte[] pidHex; private readonly byte[] machineHash; private readonly UTF8Encoding utf8 = new UTF8Encoding(false); private readonly DateTime unixEpoch = new DateTime(1970,1,DateTimeKind.Utc); public ObjectIdFactory() { MD5 md5 = MD5.Create(); machineHash = md5.ComputeHash(utf8.GetBytes(Dns.GetHostName())); pidHex = BitConverter.GetBytes(Process.GetCurrentProcess().Id); Array.Reverse(pidHex); } /// <summary> /// 產生一個新的 24 位唯一編號 /// </summary> /// <returns></returns> public ObjectId NewId() { int copyIdx = 0; byte[] hex = new byte[12]; byte[] time = BitConverter.GetBytes(GetTimestamp()); Array.Reverse(time); Array.Copy(time,hex,copyIdx,4); copyIdx += 4; Array.Copy(machineHash,3); copyIdx += 3; Array.Copy(pidHex,2,2); copyIdx += 2; byte[] inc = BitConverter.GetBytes(GetIncrement()); Array.Reverse(inc); Array.Copy(inc,3); return new ObjectId(hex); } private int GetIncrement() => System.Threading.Interlocked.Increment(ref increment); private int GetTimestamp() => Convert.ToInt32(Math.Floor((DateTime.UtcNow - unixEpoch).TotalSeconds)); }
ObjectIdFactory 的內部實現非常的簡單,但是也是整個 Oid 程式的核心,在建構函式中獲取機器名稱和程序編號以備後續生產使用,在核心方法 NewId 中,依次將 Timestamp、machineHash、pidHex、increment 寫入陣列中,最後呼叫 new ObjectId(hex) 返回生產好的 Oid。
類 ObjectId 的程式碼實現
類 ObjectId 的程式碼實現 public class ObjectId { private readonly static ObjectIdFactory factory = new ObjectIdFactory(); public ObjectId(byte[] hexData) { this.Hex = hexData; ReverseHex(); } public override string ToString() { if (Hex == null) Hex = new byte[12]; StringBuilder hexText = new StringBuilder(); for (int i = 0; i < this.Hex.Length; i++) { hexText.Append(this.Hex[i].ToString("x2")); } return hexText.ToString(); } public override int GetHashCode() => ToString().GetHashCode(); public ObjectId(string value) { if (string.IsNullOrEmpty(value)) throw new ArgumentNullException("value"); if (value.Length != 24) throw new ArgumentOutOfRangeException("value should be 24 characters"); Hex = new byte[12]; for (int i = 0; i < value.Length; i += 2) { try { Hex[i / 2] = Convert.ToByte(value.Substring(i,2),16); } catch { Hex[i / 2] = 0; } } ReverseHex(); } private void ReverseHex() { int copyIdx = 0; byte[] time = new byte[4]; Array.Copy(Hex,time,4); Array.Reverse(time); this.Timestamp = BitConverter.ToInt32(time,0); copyIdx += 4; byte[] mid = new byte[4]; Array.Copy(Hex,mid,3); this.Machine = BitConverter.ToInt32(mid,0); copyIdx += 3; byte[] pids = new byte[4]; Array.Copy(Hex,pids,2); Array.Reverse(pids); this.ProcessId = BitConverter.ToInt32(pids,0); copyIdx += 2; byte[] inc = new byte[4]; Array.Copy(Hex,inc,3); Array.Reverse(inc); this.Increment = BitConverter.ToInt32(inc,0); } public static ObjectId NewId() => factory.NewId(); public int CompareTo(ObjectId other) { if (other is null) return 1; for (int i = 0; i < Hex.Length; i++) { if (Hex[i] < other.Hex[i]) return -1; else if (Hex[i] > other.Hex[i]) return 1; } return 0; } public bool Equals(ObjectId other) => CompareTo(other) == 0; public static bool operator <(ObjectId a,ObjectId b) => a.CompareTo(b) < 0; public static bool operator <=(ObjectId a,ObjectId b) => a.CompareTo(b) <= 0; public static bool operator ==(ObjectId a,ObjectId b) => a.Equals(b); public override bool Equals(object obj) => base.Equals(obj); public static bool operator !=(ObjectId a,ObjectId b) => !(a == b); public static bool operator >=(ObjectId a,ObjectId b) => a.CompareTo(b) >= 0; public static bool operator >(ObjectId a,ObjectId b) => a.CompareTo(b) > 0; public static implicit operator string(ObjectId objectId) => objectId.ToString(); public static implicit operator ObjectId(string objectId) => new ObjectId(objectId); public static ObjectId Empty { get { return new ObjectId("000000000000000000000000"); } } public byte[] Hex { get; private set; } public int Timestamp { get; private set; } public int Machine { get; private set; } public int ProcessId { get; private set; } public int Increment { get; private set; } }
ObjectId 的程式碼量看起來稍微多一些,但是實際上,核心的實現方法就只有 ReverseHex() 方法,該方法在內部反向了 ObjectIdFactory.NewId() 的過程,使得呼叫者可以通過呼叫 ObjectId.Timestamp 等公開屬性反向追溯 Oid 的生產過程。
其它的物件比較、到 string/ObjectId 的隱式轉換,則是一些語法糖式的工作,都是為了提高編碼效率的。
需要注意的是,在類 ObjectId 的內部,建立了靜態物件 ObjectIdFactory,我們還記得在 ObjectIdFactory 的建構函式內部的初始化工作,這裡建立的靜態物件,也是為了提高生產效率的設計。
呼叫示例
在完成了程式碼改造後,我們就可以對改造後的程式碼進行呼叫測試,以驗證程式的正確性。
NewId
我們嘗試生產一組 Oid 看看效果。
for (int i = 0; i < 100; i++) { var oid = ObjectId.NewId(); Console.WriteLine(oid); }
輸出
通過上圖可以看到,輸出的這部分 Oid 都是有序的,這應該也可以成為替換 GUID/UUID 的一個理由。
生產/解包
var sourceId = ObjectId.NewId(); var reverseId = new ObjectId(sourceId);
通過解包可以看出,上圖兩個紅框內的值是一致的,解包成功!
隱式轉換
var sourceId = ObjectId.NewId(); // 轉換為 string var stringId = sourceId; string userId= ObjectId.NewId(); // 轉換為 ObjectId ObjectId id = stringId;
隱式轉換可以提高編碼效率喲!
結束語
通過上面的程式碼實現,融入了一些自己的需求。現在,可以通過解包來實現業務的追蹤和日誌的排查,在某些場景下,是非常有幫助的,增加的隱式轉換語法糖,也可以讓編碼效率得到提高;同時將程式碼優化到 .NETCore 3.1,也使用了一些 C# 的語法糖。
以上就是.NET Core中實現ObjectId反解的方法的詳細內容,更多關於.NET Core ObjectId反解的資料請關注我們其它相關文章!