前言

在设计数据库的时候,我们通常需要给业务数据表分配主键,很多时候,为了省事,我都是直接使用 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 的代码实现

  1. public class ObjectIdFactory
  2. {
  3. private int increment;
  4. private readonly byte[] pidHex;
  5. private readonly byte[] machineHash;
  6. private readonly UTF8Encoding utf8 = new UTF8Encoding(false);
  7. private readonly DateTime unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
  8. public ObjectIdFactory()
  9. {
  10. MD5 md5 = MD5.Create();
  11. machineHash = md5.ComputeHash(utf8.GetBytes(Dns.GetHostName()));
  12. pidHex = BitConverter.GetBytes(Process.GetCurrentProcess().Id);
  13. Array.Reverse(pidHex);
  14. }
  15. /// <summary>
  16. /// 产生一个新的 24 位唯一编号
  17. /// </summary>
  18. /// <returns></returns>
  19. public ObjectId NewId()
  20. {
  21. int copyIdx = 0;
  22. byte[] hex = new byte[12];
  23. byte[] time = BitConverter.GetBytes(GetTimestamp());
  24. Array.Reverse(time);
  25. Array.Copy(time, 0, hex, copyIdx, 4);
  26. copyIdx += 4;
  27. Array.Copy(machineHash, 0, hex, copyIdx, 3);
  28. copyIdx += 3;
  29. Array.Copy(pidHex, 2, hex, copyIdx, 2);
  30. copyIdx += 2;
  31. byte[] inc = BitConverter.GetBytes(GetIncrement());
  32. Array.Reverse(inc);
  33. Array.Copy(inc, 1, hex, copyIdx, 3);
  34. return new ObjectId(hex);
  35. }
  36. private int GetIncrement() => System.Threading.Interlocked.Increment(ref increment);
  37. private int GetTimestamp() => Convert.ToInt32(Math.Floor((DateTime.UtcNow - unixEpoch).TotalSeconds));
  38. }

ObjectIdFactory 的内部实现非常的简单,但是也是整个 Oid 程序的核心,在构造函数中获取机器名称和进程编号以备后续生产使用,在核心方法 NewId 中,依次将 Timestamp、machineHash、pidHex、increment 写入数组中,最后调用 new ObjectId(hex) 返回生产好的 Oid。

类 ObjectId 的代码实现

  1. public class ObjectId
  2. {
  3. private readonly static ObjectIdFactory factory = new ObjectIdFactory();
  4. public ObjectId(byte[] hexData)
  5. {
  6. this.Hex = hexData;
  7. ReverseHex();
  8. }
  9. public override string ToString()
  10. {
  11. if (Hex == null)
  12. Hex = new byte[12];
  13. StringBuilder hexText = new StringBuilder();
  14. for (int i = 0; i < this.Hex.Length; i++)
  15. {
  16. hexText.Append(this.Hex[i].ToString("x2"));
  17. }
  18. return hexText.ToString();
  19. }
  20. public override int GetHashCode() => ToString().GetHashCode();
  21. public ObjectId(string value)
  22. {
  23. if (string.IsNullOrEmpty(value)) throw new ArgumentNullException("value");
  24. if (value.Length != 24) throw new ArgumentOutOfRangeException("value should be 24 characters");
  25. Hex = new byte[12];
  26. for (int i = 0; i < value.Length; i += 2)
  27. {
  28. try
  29. {
  30. Hex[i / 2] = Convert.ToByte(value.Substring(i, 2), 16);
  31. }
  32. catch
  33. {
  34. Hex[i / 2] = 0;
  35. }
  36. }
  37. ReverseHex();
  38. }
  39. private void ReverseHex()
  40. {
  41. int copyIdx = 0;
  42. byte[] time = new byte[4];
  43. Array.Copy(Hex, copyIdx, time, 0, 4);
  44. Array.Reverse(time);
  45. this.Timestamp = BitConverter.ToInt32(time, 0);
  46. copyIdx += 4;
  47. byte[] mid = new byte[4];
  48. Array.Copy(Hex, copyIdx, mid, 0, 3);
  49. this.Machine = BitConverter.ToInt32(mid, 0);
  50. copyIdx += 3;
  51. byte[] pids = new byte[4];
  52. Array.Copy(Hex, copyIdx, pids, 0, 2);
  53. Array.Reverse(pids);
  54. this.ProcessId = BitConverter.ToInt32(pids, 0);
  55. copyIdx += 2;
  56. byte[] inc = new byte[4];
  57. Array.Copy(Hex, copyIdx, inc, 0, 3);
  58. Array.Reverse(inc);
  59. this.Increment = BitConverter.ToInt32(inc, 0);
  60. }
  61. public static ObjectId NewId() => factory.NewId();
  62. public int CompareTo(ObjectId other)
  63. {
  64. if (other is null)
  65. return 1;
  66. for (int i = 0; i < Hex.Length; i++)
  67. {
  68. if (Hex[i] < other.Hex[i])
  69. return -1;
  70. else if (Hex[i] > other.Hex[i])
  71. return 1;
  72. }
  73. return 0;
  74. }
  75. public bool Equals(ObjectId other) => CompareTo(other) == 0;
  76. public static bool operator <(ObjectId a, ObjectId b) => a.CompareTo(b) < 0;
  77. public static bool operator <=(ObjectId a, ObjectId b) => a.CompareTo(b) <= 0;
  78. public static bool operator ==(ObjectId a, ObjectId b) => a.Equals(b);
  79. public override bool Equals(object obj) => base.Equals(obj);
  80. public static bool operator !=(ObjectId a, ObjectId b) => !(a == b);
  81. public static bool operator >=(ObjectId a, ObjectId b) => a.CompareTo(b) >= 0;
  82. public static bool operator >(ObjectId a, ObjectId b) => a.CompareTo(b) > 0;
  83. public static implicit operator string(ObjectId objectId) => objectId.ToString();
  84. public static implicit operator ObjectId(string objectId) => new ObjectId(objectId);
  85. public static ObjectId Empty { get { return new ObjectId("000000000000000000000000"); } }
  86. public byte[] Hex { get; private set; }
  87. public int Timestamp { get; private set; }
  88. public int Machine { get; private set; }
  89. public int ProcessId { get; private set; }
  90. public int Increment { get; private set; }
  91. }

ObjectId 的代码量看起来稍微多一些,但是实际上,核心的实现方法就只有 ReverseHex() 方法,该方法在内部反向了 ObjectIdFactory.NewId() 的过程,使得调用者可以通过调用 ObjectId.Timestamp 等公开属性反向追溯 Oid 的生产过程。

其它的对象比较、到 string/ObjectId 的隐式转换,则是一些语法糖式的工作,都是为了提高编码效率的。

需要注意的是,在类 ObjectId 的内部,创建了静态对象 ObjectIdFactory,我们还记得在 ObjectIdFactory 的构造函数内部的初始化工作,这里创建的静态对象,也是为了提高生产效率的设计。

调用示例

在完成了代码改造后,我们就可以对改造后的代码进行调用测试,以验证程序的正确性。

NewId

我们尝试生产一组 Oid 看看效果。

  1. for (int i = 0; i < 100; i++)
  2. {
  3. var oid = ObjectId.NewId();
  4. Console.WriteLine(oid);
  5. }

输出

通过上图可以看到,输出的这部分 Oid 都是有序的,这应该也可以成为替换 GUID/UUID 的一个理由。

生产/解包

  1. var sourceId = ObjectId.NewId();
  2. var reverseId = new ObjectId(sourceId);

通过解包可以看出,上图两个红框内的值是一致的,解包成功!

隐式转换

  1. var sourceId = ObjectId.NewId();
  2. // 转换为 string
  3. var stringId = sourceId;
  4. string userId= ObjectId.NewId();
  5. // 转换为 ObjectId
  6. ObjectId id = stringId;

隐式转换可以提高编码效率哟!

结束语

通过上面的代码实现,融入了一些自己的需求。现在,可以通过解包来实现业务的追踪和日志的排查,在某些场景下,是非常有帮助的,增加的隐式转换语法糖,也可以让编码效率得到提高;同时将代码优化到 .NETCore 3.1,也使用了一些 C# 的语法糖。

.NETCore中实现ObjectId反解的更多相关文章

  1. ubuntu中的Wine详解

    什么是wine?(转自百度百科,具体看百科) wine,是一款优秀的Linux系统平台下的模拟器软件,用来将Windows系统下的软件在Linux系统下稳定运行,该软件更新频繁,日臻完善,可以运行许多 ...

  2. 【转】ubuntu中的Wine详解

    原文网址:http://blog.csdn.net/iwtwiioi/article/details/10530561 什么是wine?(转自百度百科,具体看百科) wine,是一款优秀的Linux系 ...

  3. Django学习之十一:真正理解Django的路由分发和反解url原理

    目录 URL Dispatcher 简介 模式概念 对比URLPattern 与 URLResolver (多态的体现) 构建子路由几种方式 反解url算法逻辑 URL Dispatcher 简介 d ...

  4. angularJS中$apply()方法详解

    这篇文章主要介绍了angularJS中$apply()方法详解,需要的朋友可以参考下   对于一个在前端属于纯新手的我来说,Javascript都还是一知半解,要想直接上手angular JS,遇到的 ...

  5. python中常用模块详解二

    log模块的讲解 Python 使用logging模块记录日志涉及四个主要类,使用官方文档中的概括最为合适: logger提供了应用程序可以直接使用的接口API: handler将(logger创建的 ...

  6. ALSA声卡驱动中的DAPM详解之四:在驱动程序中初始化并注册widget和route

    前几篇文章我们从dapm的数据结构入手,了解了代表音频控件的widget,代表连接路径的route以及用于连接两个widget的path.之前都是一些概念的讲解以及对数据结构中各个字段的说明,从本章开 ...

  7. k8s 证书反解

    k8s证书反解 1.将k8s配置文件(kubelet.kubeconfig)中client-certificate-data:内容拷贝 2.echo "client-certificate- ...

  8. 反解ios静态库

    p.p1 { margin: 0; font: 12px "Helvetica Neue" } p.p2 { margin: 0; font: 12px "Helveti ...

  9. .NetCore中的日志(2)集成第三方日志工具

    .NetCore中的日志(2)集成第三方日志工具 0x00 在.NetCore的Logging组件中集成NLog 上一篇讨论了.NetCore中日志框架的结构,这一篇讨论一下.NetCore的Logg ...

随机推荐

  1. 深克隆(deepclone)

    1.简单版: <script type="text/javascript"> const newObj = JSON.parse(JSON.stringify(oldO ...

  2. java 数据结构(六):数组与集合

    1. 集合与数组存储数据概述:集合.数组都是对多个数据进行存储操作的结构,简称Java容器.说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(.txt,.jpg,.avi,数据库中) ...

  3. scrapy 源码解析 (五):启动流程源码分析(五) Scraper刮取器

    Scraper刮取器 对ExecutionEngine执行引擎篇出现的Scraper进行展开.Scraper的主要作用是对spider中间件进行管理,通过中间件完成请求.响应.数据分析等工作. Scr ...

  4. cmake安装使用

    1.安装命令: yum install -y gcc gcc-c++ make automake wget http://www.cmake.org/files/v2.8/cmake-2.8.10.2 ...

  5. redis入门指南(四)—— redis如何节省空间

    写在前面 学习<redis入门指南>笔记,结合实践,只记录重要,明确,属于新知的相关内容. 节省空间 1.redis对于它所支持的五种数据类型,每种都提供了两种及以上的编码方式去存储(具体 ...

  6. Vue中token的实现

    在学习vue的过程中,正好项目中做的web系统对安全性有要求 转载自https://www.jianshu.com/p/d1a3fb71eb99 总:通过axios,vuex,及自定义的方法实现.以下 ...

  7. 定时器三----js定时器

    方法一:        var t;        //初始化定时器    $(function(){        init_fun_timer1();            });         ...

  8. Qt-数据库操作SQLite

    1  简介 参考视频:https://www.bilibili.com/video/BV1XW411x7NU?p=88 说明:本文对在Qt中操作SQLite做简要说明. SQLite:SQLite 是 ...

  9. go : 连接数据库并插入数据

      package main import ( "database/sql" "fmt" "log" "net/http" ...

  10. C++语法小记---类型检测

    类型检测 C++使用typeid关键字进行类型检查 不同的编译器使用typeid返回的类型名称不严格一致,需要特别注意 也可以使用虚函数,返回各自的类型名 如果typeid的操作数不是类类型(类指针也 ...