.NETCore中實現ObjectId反解

前言

在設計數據庫的時候,咱們一般須要給業務數據表分配主鍵,不少時候,爲了省事,我都是直接使用 GUID/UUID 的方式,可是在 MonggoDB 中,其內部實現了 ObjectId(如下統稱爲Oid)。而且在.NETCore 的驅動中給出了源代碼的實現。算法

通過仔細研讀官方的源碼後發現,其實現原理很是的簡單易學,在最新的版本中,閹割了 UnPack 函數,多是官方以爲解包是沒什麼太多的使用場景的,可是咱們認爲,對於數據溯源來講,解包的操做實在是很是有必要,特別是在目前的微服務大流行的背景下。數據庫

爲此,在參考官方代碼的基礎上進行了部分改進,增長了一下本身的需求。本示例代碼增長了解包的操做、對 string 的隱式轉換、提供讀取解包後數據的公開屬性。數組

ObjectId 的數據結構

首先,咱們來看 Oid 的數據結構的設計。安全

從上圖能夠看出,Oid 的數據結構主要由四個部分組成,分別是:Unix時間戳、機器名稱、進程編號、自增編號。Oid 其實是總長度爲12個字節24的字符串,易記口訣爲:4323,時間4字節,機器名3字節,進程編號2字節,自增編號3字節。數據結構

一、Unix時間戳:Unix時間戳以秒爲記錄單位,即從1970/1/1 00:00:00 開始到當前時間的總秒數。
二、機器名稱:記錄當前生產Oid的設備號
三、進程編號:當前運行Oid程序的編號
四、自增編號:在當前秒內,每次調用都將自動增加(已實現線程安全)ide

根據算法可知,當前一秒內產生的最大 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, 1, 0, 0, 0, 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, 0, hex, copyIdx, 4);
        copyIdx += 4;

        Array.Copy(machineHash, 0, hex, copyIdx, 3);
        copyIdx += 3;

        Array.Copy(pidHex, 2, hex, copyIdx, 2);
        copyIdx += 2;

        byte[] inc = BitConverter.GetBytes(GetIncrement());
        Array.Reverse(inc);
        Array.Copy(inc, 1, hex, copyIdx, 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 的代碼實現

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, copyIdx, time, 0, 4);
        Array.Reverse(time);
        this.Timestamp = BitConverter.ToInt32(time, 0);
        copyIdx += 4;
        byte[] mid = new byte[4];
        Array.Copy(Hex, copyIdx, mid, 0, 3);
        this.Machine = BitConverter.ToInt32(mid, 0);
        copyIdx += 3;
        byte[] pids = new byte[4];
        Array.Copy(Hex, copyIdx, pids, 0, 2);
        Array.Reverse(pids);
        this.ProcessId = BitConverter.ToInt32(pids, 0);
        copyIdx += 2;
        byte[] inc = new byte[4];
        Array.Copy(Hex, copyIdx, inc, 0, 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# 的語法糖。

相關文章
相關標籤/搜索