GUID 作为多个数据库下的快速主键






4.92/5 (88投票s)
采用正确的方法,可以在几乎任何数据库系统上使 GUID 几乎与整数主键一样快。
介绍
本文概述了一种使用 GUID 值作为主键/聚集索引的方法,该方法避免了大多数常见缺点,并根据 Jimmy Nilsson 在其文章 GUID 作为主键的成本 中开发的 COMB 模型进行顺序 GUID 调整。虽然该基本模型已被各种库和框架(包括 NHibernate)使用,但大多数实现似乎是特定于 Microsoft SQL Server 的。本文试图将该方法改编为一种灵活的系统,可与 Oracle、PostgreSQL 和 MySQL 等其他常见数据库系统一起使用,并特别解决了 .NET Framework 的一些特殊性。
背景
历史上,一种非常常见的数据库设计模型使用顺序整数来标识数据行,通常由服务器本身在插入新行时生成。这是一种简单、清晰的方法,适用于许多应用程序。
然而,在某些情况下它并不理想。随着对象关系映射 (ORM) 框架(如 NHibernate 和 ADO.NET Entity Framework)的日益普及,依赖服务器生成键值会增加许多人宁愿避免的复杂性。同样,复制场景也使得依赖单一权威源创建键值变得有问题——其全部目的是最大限度地减少单一权威的作用。
一种诱人的替代方案是使用 GUID 作为键值。GUID(全局唯一标识符),也称为 UUID,是一个 128 位的值,它合理地保证在所有空间和时间中都是唯一的。创建 GUID 的标准在 RFC 4122 中描述,但目前常用的大多数 GUID 创建算法要么本质上是一个非常长的随机数,要么将随机出现的组件与本地系统的某种标识信息(例如网络 MAC 地址)结合起来。
GUID 的优势在于允许开发人员即时创建新的键值,而无需与服务器核对,也无需担心该值可能已被其他人使用。乍一看,它们似乎为问题提供了一个很好的解决方案。
那么问题出在哪里?嗯,性能。为了获得最佳性能,大多数数据库将行存储在所谓的*聚集索引*中,这意味着表中的行实际上是按排序顺序存储在磁盘上的,通常基于主键值。这使得查找单行就像在索引中快速查找一样简单,但如果新行的主键不在列表末尾,则向表中添加新行可能会非常慢。例如,考虑以下数据
ID | 名称 |
1 | 福尔摩斯,S。 |
4 | 华生,J。 |
7 | 莫里亚蒂,J。 |
到目前为止很简单:行按照 ID 列的值按顺序存储。如果我们添加一个 ID 为 8 的新行,这没有问题:该行只是添加到末尾。
ID | 名称 |
1 | 福尔摩斯,S。 |
4 | 华生,J。 |
7 | 莫里亚蒂,J。 |
8 | 雷斯垂德,I。 |
但现在假设我们要插入一个 ID 为 5 的行
ID | 名称 |
1 | 福尔摩斯,S。 |
4 | 华生,J。 |
5 | 哈德逊太太 |
7 | 莫里亚蒂,J。 |
8 | 雷斯垂德,I。 |
行 7 和 8 必须向下移动以腾出空间。这里不是什么大问题,但是当您谈论在包含数百万行的表的中间插入内容时,它就开始成为一个问题。当您想每秒执行一百次时,它真的会累积起来。
这就是 GUID 的问题:它们可能真正随机,也可能不真正随机,但它们大多数*看起来*随机,因为它们通常不会生成任何特定类型的顺序。因此,在任何重要大小的数据库中,将 GUID 值用作主键的一部分通常被认为是非常糟糕的做法。插入可能会非常慢,并且涉及大量不必要的磁盘活动。
顺序 GUID
那么,解决方案是什么?嗯,GUID 的主要问题是它们缺乏序列。所以,让我们添加一个序列。COMB 方法(COMBined GUID/timestamp 的缩写)用一个保证会增加,或至少不会减少的值替换 GUID 的一部分,每次生成新值时都会如此。顾名思义,它通过使用当前日期和时间生成的值来实现这一点。
为了说明,请考虑以下典型 GUID 值列表:
fda437b5-6edd-42dc-9bbd-c09d10460ad0
2cb56c59-ef3d-4d24-90e7-835ed5968cdc
6bce82f3-5bd2-4efc-8832-986227592f26
42af7078-4b9c-4664-ba01-0d492ba3bd83
现在考虑这个特殊的 GUID 值假设列表:
00000001-a411-491d-969a-77bf40f55175
00000002-d97d-4bb9-a493-cad277999363
00000003-916c-4986-a363-0a9b9c95ca52
00000004-f827-452b-a3be-b77a3a4c95aa
第一组数字已被递增序列替换——例如,自程序启动以来的毫秒数。插入一百万行这些值不会那么糟糕,因为每行只会简单地附加到列表的末尾,而不需要对现有数据进行任何重新洗牌。
现在我们有了基本概念,我们需要深入了解 GUID 的构造方式以及不同数据库系统如何处理它们的一些细节。
128 位 GUID 由四个主要块组成,称为 Data1、Data2、Data3 和 Data4,您可以在下面的示例中看到:
11111111-2222-3333-4444-444444444444
Data1 是四个字节,Data2 是两个字节,Data3 是两个字节,Data4 是八个字节(Data3 的一些位和 Data4 的第一部分保留用于版本信息,但这或多或少是结构)。
目前使用的大多数 GUID 算法,尤其是 .NET Framework 使用的算法,几乎都是花哨的随机数生成器(Microsoft 过去曾将本地机器的 MAC 地址作为 GUID 的一部分,但由于隐私问题在几年前停止了这种做法)。这对我们来说是个好消息,因为这意味着修改值的不同部分不太可能损害值的唯一性。
但对我们来说不幸的是,不同的数据库以不同的方式处理 GUID。一些系统(Microsoft SQL Server、PostgreSQL)具有内置的 GUID 类型,可以直接存储和操作 GUID。没有原生 GUID 支持的数据库有不同的约定来模拟它们。例如,MySQL 最常通过将其字符串表示写入 char(36) 列来存储 GUID。Oracle 通常将 GUID 值的原始字节存储在 raw(16) 列中。
情况变得更加复杂,因为 Microsoft SQL Server 的一个特殊性是它根据最不重要的六个字节(即 Data4 块的最后六个字节)对 GUID 值进行排序。因此,如果我们要创建一个用于 SQL Server 的顺序 GUID,我们必须将顺序部分放在末尾。大多数其他数据库系统会希望它在开头。
算法
查看数据库处理 GUID 的不同方式,很明显,不可能存在一种适用于所有情况的顺序 GUID 算法;我们必须针对特定应用程序进行定制。经过一些实验,我确定了三种主要方法,涵盖了几乎所有用例
- 创建存储为字符串时是顺序的 GUID
- 创建存储为二进制数据时是顺序的 GUID
- 创建在 SQL Server 上是顺序的 GUID,顺序部分在末尾
(为什么存储为字符串的 GUID 与存储为字节的 GUID 不同?因为 .NET 处理 GUID 的方式,在小端系统(大多数可能运行 .NET 的机器)上,字符串表示可能不是您所期望的。稍后会详细介绍。)
我已将这些选择在代码中表示为枚举
public enum SequentialGuidType
{
SequentialAsString,
SequentialAsBinary,
SequentialAtEnd
}
现在我们可以定义一个方法来生成我们的 GUID,该方法接受其中一个枚举值,并相应地调整结果
public Guid NewSequentialGuid(SequentialGuidType guidType)
{
...
}
但是我们究竟如何创建顺序 GUID 呢?它的哪一部分我们保留“随机”,哪一部分我们用时间戳替换?嗯,原始的 COMB 规范,为 SQL Server 定制,用时间戳值替换了 Data4 的最后六个字节。这部分是出于方便,因为这六个字节是 SQL Server 用来排序 GUID 值的部分,但六个字节作为时间戳是足够好的平衡。这为随机组件留下了十个字节。
对我来说最有意义的是从一个新的随机 GUID 开始。就像我刚才说的,我们需要十个随机字节:
var rng = new System.Security.Cryptography.RNGCryptoServiceProvider();
byte[] randomBytes = new byte[10];
rng.GetBytes(randomBytes);
我们使用 `RNGCryptoServiceProvider` 来生成我们的随机组件,因为 `System.Random` 存在一些使其不适合此目的的缺陷(例如,它生成的数字遵循一些可识别的模式,并且在不超过 2^32 次迭代后会循环)。由于我们依赖随机性来尽可能地保证唯一性,因此确保我们的初始状态尽可能地强随机性符合我们的利益,而 `RNGCryptoServiceProvider` 提供了加密强的随机数据。
(然而,它的速度也相对较慢,因此如果性能至关重要,您可能需要考虑另一种方法——例如,简单地使用 `Guid.NewGuid()` 的数据初始化字节数组。我避免了这种方法,因为 `Guid.NewGuid()` 本身不对随机性做任何保证;那只是当前实现的工作方式。因此,我选择谨慎行事,坚持使用我*知道*会可靠运行的方法。)
好的,我们现在有了新值的随机部分,剩下的就是用我们的时间戳替换它的一部分。我们决定使用一个六字节的时间戳,但它应该基于什么呢?一个显而易见的选择是使用 `DateTime.Now`(或者,正如 Rich Andersen 指出的那样,为了更好的性能,使用 `DateTime.UtcNow`)并以某种方式将其转换为一个六字节的整数值。`Ticks` 属性很诱人:它返回自公元 0001 年 1 月 1 日以来经过的 100 纳秒间隔数。然而,这里有几个问题。
首先,由于 `Ticks` 返回一个 64 位整数,而我们只有 48 位可用,所以我们必须截断两个字节,剩下的 48 位 100 纳秒间隔会使它在溢出和循环之前不到一年。这将破坏我们试图建立的顺序排序,并摧毁我们希望获得的性能提升,并且由于许多应用程序将服务超过一年,我们必须使用一个精度较低的时间度量。
另一个困难是 `DateTime.UtcNow` 的分辨率有限。根据文档,该值可能只每 10 毫秒更新一次。(它似乎在某些系统上更新更频繁,但我们不能依赖这一点。)
好消息是,这两个问题某种程度上相互抵消:有限的分辨率意味着使用整个 `Ticks` 值没有意义。所以,我们不直接使用 tick,而是除以 10,000,得到自公元 0001 年 1 月 1 日以来经过的毫秒数,然后取其最不重要的 48 位作为我们的时间戳。我使用毫秒是因为,即使 `DateTime.UtcNow` 在某些系统上目前限制为 10 毫秒的分辨率,未来可能会有所改进,我想为此留出空间。将时间戳的分辨率降低到毫秒也使我们在大约公元 5800 年之前不会溢出和循环;希望这对于大多数应用程序来说已经足够。
在继续之前,关于这种方法的一个简短脚注:使用 1 毫秒分辨率的时间戳意味着非常接近地生成的 GUID 可能具有相同的时间戳值,因此不会是顺序的。这对于某些应用程序来说可能很常见,事实上我尝试了一些替代方法,例如使用更高分辨率的计时器,如 `System.Diagnostics.Stopwatch`,或者将时间戳与一个“计数器”结合起来,以保证序列持续到时间戳更新。然而,在测试期间我发现这根本没有明显差异,即使在同一个 1 毫秒窗口内生成了数十甚至数百个 GUID。这与 Jimmy Nilsson 在使用 COMB 进行测试时遇到的情况一致。考虑到这一点,我选择了这里概述的方法,因为它简单得多。
这是代码
long timestamp = DateTime.UtcNow.Ticks / 10000L;
byte[] timestampBytes = BitConverter.GetBytes(timestamp);
现在我们有了时间戳。然而,由于我们使用 `BitConverter` 从数值中获取字节,我们必须考虑字节顺序。
if (BitConverter.IsLittleEndian) { Array.Reverse(timestampBytes); }
我们有了 GUID 随机部分的字节,也有了时间戳的字节,所以剩下的就是将它们组合起来。此时,我们必须根据传入方法中的 `SequentialGuidType` 值来定制格式。对于 `SequentialAsBinary` 和 `SequentialAsString` 类型,我们先复制时间戳,然后是随机组件。对于 `SequentialAtEnd` 类型,则相反。
byte[] guidBytes = new byte[16]; switch (guidType) { case SequentialGuidType.SequentialAsString: case SequentialGuidType.SequentialAsBinary: Buffer.BlockCopy(timestampBytes, 2, guidBytes, 0, 6); Buffer.BlockCopy(randomBytes, 0, guidBytes, 6, 10); break; case SequentialGuidType.SequentialAtEnd: Buffer.BlockCopy(randomBytes, 0, guidBytes, 0, 10); Buffer.BlockCopy(timestampBytes, 2, guidBytes, 10, 6); break; }
到目前为止一切顺利。但现在我们遇到了 .NET Framework 的一个特殊之处:它不仅仅将 GUID 视为字节序列。出于某种原因,它将 GUID 视为一个包含 32 位整数、两个 16 位整数和八个单独字节的结构。换句话说,它将 Data1 块视为 `Int32`,Data2 和 Data3 块视为两个 `Int16`,Data4 块视为 `Byte[8]`。
这对我们意味着什么?嗯,主要问题又与字节顺序有关。由于 .NET 认为它正在处理数值,我们必须在小端系统上进行补偿——但是!——仅适用于将 GUID 值转换为字符串,并且时间戳部分位于 GUID 开头的应用程序(时间戳部分位于末尾的应用程序在 GUID 的“数字”部分中没有任何重要内容,因此我们无需对它们执行任何操作)。
这就是我上面提到的区分将作为字符串存储的 GUID 和将作为二进制数据存储的 GUID 的原因。对于将其作为字符串存储的数据库,ORM 框架和应用程序可能希望使用 `ToString()` 方法生成 SQL INSERT 语句,这意味着我们必须纠正字节序问题。对于将其作为二进制数据存储的数据库,它们可能会使用 `Guid.ToByteArray()` 生成 INSERT 语句的字符串,这意味着不需要纠正。所以,我们还有最后一件事要添加:
if (guidType == SequentialGuidType.SequentialAsString &&
BitConverter.IsLittleEndian)
{
Array.Reverse(guidBytes, 0, 4);
Array.Reverse(guidBytes, 4, 2);
}
现在我们完成了,我们可以使用字节数组来构造并返回一个 GUID。
return new Guid(guidBytes);
使用代码
要使用我们的方法,我们首先必须确定哪种类型的 GUID 最适合我们的数据库和我们正在使用的任何 ORM 框架。以下是一些常见数据库类型的大致经验法则,尽管这些可能会根据您的应用程序细节而有所不同
数据库 | GUID 列 | SequentialGuidType 值 |
Microsoft SQL Server | uniqueidentifier | SequentialAtEnd
|
MySQL | char(36) | SequentialAsString |
Oracle | raw(16) | SequentialAsBinary |
PostgreSQL | uuid | SequentialAsString |
SQLite | 可变 | 可变 |
(对于 SQLite 数据库,没有原生 GUID 列类型,但有模仿该功能的扩展。然而,GUID 值在内部可能存储为 16 字节的二进制数据,或 36 个字符的文本,具体取决于连接字符串中传入的 `BinaryGUID` 参数的值,因此没有“一刀切”的答案。)
以下是使用我们新方法生成的一些示例。
首先,NewSequentialGuid(SequentialGuidType.SequentialAsString):
39babcb4-e446-4ed5-4012-2e27653a9d13
39babcb4-e447-ae68-4a32-19eb8d91765d
39babcb4-e44a-6c41-0fb4-21edd4697f43
39babcb4-e44d-51d2-c4b0-7d8489691c70
如您所见,前六个字节(前两个块)按顺序排列,其余部分是随机的。将这些值插入以字符串形式存储 GUID 的数据库(如 MySQL)应该会比非顺序值提供性能提升。
接下来,NewSequentialGuid(SequentialGuidType.SequentialAtEnd):
a47ec5e3-8d62-4cc1-e132-39babcb4e47a
939aa853-5dc9-4542-0064-39babcb4e47c
7c06fdf6-dca2-4a1a-c3d7-39babcb4e47d
c21a4d6f-407e-48cf-656c-39babcb4e480
正如我们所预料的,最后六个字节是顺序的,其余是随机的。我不知道为什么 SQL Server 会以这种方式排序 `uniqueidentifier` 索引,但它确实如此,而且这应该会很好用。
最后是 NewSequentialGuid(SequentialGuidType.SequentialAsBinary)
b4bcba39-58eb-47ce-8890-71e7867d67a5
b4bcba39-5aeb-42a0-0b11-db83dd3c635b
b4bcba39-6aeb-4129-a9a5-a500aac0c5cd
b4bcba39-6ceb-494d-a978-c29cef95d37f
当以 `ToString()` 输出的格式在此处查看时,我们可以看到有些地方看起来不对劲。前两个块由于所有字节都被反转而“混乱”(这是由于前面讨论的字节序问题)。如果我们将这些值插入到文本字段中(就像它们在 MySQL 中那样),性能将不会理想。
然而,这个问题的出现是因为列表中这四个值是使用 `ToString()` 方法生成的。假设相同的四个 GUID 被使用 `Guid.ToByteArray()` 返回的数组转换为十六进制字符串:
39babcb4eb5847ce889071e7867d67a5
39babcb4eb5a42a00b11db83dd3c635b
39babcb4eb6a4129a9a5a500aac0c5cd
39babcb4eb6c494da978c29cef95d37f
例如,ORM 框架最有可能以这种方式为 Oracle 数据库生成 INSERT 语句,您可以看到,当以这种方式格式化时,序列再次可见。
所以,总结一下,我们现在有了一个方法,可以为三种不同类型的数据库生成顺序 GUID 值:那些以字符串形式存储的(MySQL,有时是 SQLite),那些以二进制数据形式存储的(Oracle,PostgreSQL),以及 Microsoft SQL Server,它有自己奇怪的存储方案。
我们可以进一步定制我们的方法,让它根据应用程序设置中的值自动检测数据库类型,或者我们可以创建一个接受我们正在使用的 `DbConnection` 并从中确定正确类型的重载,但这将取决于应用程序和所使用的任何 ORM 框架的细节。就当是作业吧!
基准测试
为了进行测试,我重点关注了四种常见的数据库系统:Microsoft SQL Server 2008、MySQL 5.5、Oracle XE 11.2 和 PostgreSQL 9.1,所有这些都在我的桌面上的 Windows 7 下运行。(如果有人想在更多数据库类型或其他操作系统下运行测试,我很乐意尽力帮助!)
测试通过使用每个数据库系统的命令行工具将 200 万行插入到一个具有 GUID 主键和 100 字符文本字段的表中进行。使用上述三种方法中的每一种进行了一次测试,并使用 `Guid.NewGuid()` 方法作为对照进行了第四次测试。为了比较,我还进行了第五次测试,将 200 万行插入到一个具有整数主键的类似表中。在插入第一百万行后记录完成插入的时间(以秒为单位),然后在插入第二百万行后再次记录。以下是结果:
对于 SQL Server,我们期望 `SequentialAtEnd` 方法效果最好(因为它专门为 SQL Server 添加),并且情况看起来不错:使用该方法的 GUID 插入仅比整数主键慢 8.4%——这绝对可以接受。这比随机 GUID 的性能提高了 75%。您还可以看到 `SequentialAsBinary` 和 `SequentialAsString` 方法仅比随机 GUID 提供很小的优势,正如我们所期望的。另一个重要指标是,对于随机 GUID,第二百万次插入比第一百万次插入花费的时间更长,这与大量页面洗牌以维护聚集索引随着更多行添加到中间而增加的情况一致,而对于 `SequentialAtEnd` 方法,第二百万次插入花费的时间与第一百万次插入几乎相同,这表明新行只是简单地附加到表的末尾。到目前为止一切顺利。
如您所见,MySQL 在非顺序 GUID 方面性能*非常*差——差到我不得不裁剪图表的顶部才能使其他条形可读(第二百万行比第一百万行多花了超过一半的时间)。然而,`SequentialAsString` 方法的性能几乎与整数主键相同,这正是我们所期望的,因为 GUID 通常在 MySQL 中作为 char(36) 字段存储。`SequentialAsBinary` 方法的性能也类似,这可能是因为即使字节顺序不正确,这些值作为一个整体也“有点”顺序。
Oracle 很难驾驭。将 GUID 存储为 raw(16) 列,我们期望 `SequentialAsBinary` 方法最快,事实也确实如此,但即使是随机 GUID 也没有比整数慢太多。此外,顺序 GUID 插入比整数插入*更快*,这很难接受。虽然顺序 GUID 在这些基准测试中确实产生了可衡量的改进,但我不得不怀疑这里的奇怪之处是否是由于我编写 Oracle 良好批量插入的经验不足。如果有人想尝试一下,请告诉我!
最后是 PostgreSQL。与 Oracle 类似,即使是随机 GUID,性能也不算太差,但顺序 GUID 的差异更为显著。正如预期的那样,`SequentialAsString` 方法最快,仅比整数主键慢 7.8%,几乎是随机 GUID 的两倍。
附加说明
还有一些其他事项需要考虑。很多重点放在了插入顺序 GUID 的性能上,但是*创建*它们的性能呢?与 `Guid.NewGuid()` 相比,生成一个顺序 GUID 需要多长时间?嗯,它肯定更慢:在我的系统上,我可以 140 毫秒生成一百万个随机 GUID,但顺序 GUID 需要 2800 毫秒——慢了二十倍。
一些快速测试表明,大部分的慢速是由于使用 `RNGCryptoServiceProvider` 生成我们的随机数据;切换到 `System.Random` 将结果降至约 400 毫秒。然而,我仍然不建议这样做,因为 `System.Random` 在这些用途上仍然存在问题。但是,可能可以使用一种既更快又足够强的替代算法——坦率地说,我对随机数生成器知之甚少。
较慢的创建速度是否令人担忧?就我个人而言,我觉得可以接受。除非您的应用程序涉及非常频繁的插入(在这种情况下,GUID 键可能由于其他原因而不理想),否则偶尔创建 GUID 的成本与更快数据库操作带来的好处相比将显得微不足道。
另一个问题:用时间戳替换 GUID 的六个字节意味着只剩下十个字节用于随机数据。这会危及唯一性吗?嗯,这取决于具体情况。包含时间戳意味着任何两个相隔几毫秒以上创建的 GUID *保证*是唯一的——这是完全随机的 GUID(例如 `Guid.NewGuid()` 返回的 GUID)无法做到的承诺。但是非常接近地创建的 GUID 呢?嗯,十字节加密强度随机性意味着 280,即 1,208,925,819,614,629,174,706,176 种可能的组合。在几毫秒内生成的两个 GUID 具有相同随机组件的概率可能与例如数据库服务器及其所有备份被同时发生的野猪袭击摧毁的概率相比微不足道。
最后一个问题是这里生成的 GUID 在技术上不符合 RFC 4122 中指定的格式——例如,它们缺少通常占据位 48 到 51 的版本号。我个人不认为这是个大问题;我不知道有任何数据库实际上关心 GUID 的内部结构,省略版本块给了我们额外的四个随机位。但是,如果需要,我们可以很容易地将其添加回来。
最终代码
这是该方法的完整版本。上面给出的代码进行了一些小的更改(例如将随机数生成器抽象为静态实例,并对 `switch()` 块进行了一些重构)
using System;
using System.Security.Cryptography;
public enum SequentialGuidType
{
SequentialAsString,
SequentialAsBinary,
SequentialAtEnd
}
public static class SequentialGuidGenerator
{
private static readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider();
public static Guid NewSequentialGuid(SequentialGuidType guidType)
{
byte[] randomBytes = new byte[10];
_rng.GetBytes(randomBytes);
long timestamp = DateTime.UtcNow.Ticks / 10000L;
byte[] timestampBytes = BitConverter.GetBytes(timestamp);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(timestampBytes);
}
byte[] guidBytes = new byte[16];
switch (guidType)
{
case SequentialGuidType.SequentialAsString:
case SequentialGuidType.SequentialAsBinary:
Buffer.BlockCopy(timestampBytes, 2, guidBytes, 0, 6);
Buffer.BlockCopy(randomBytes, 0, guidBytes, 6, 10);
// If formatting as a string, we have to reverse the order
// of the Data1 and Data2 blocks on little-endian systems.
if (guidType == SequentialGuidType.SequentialAsString && BitConverter.IsLittleEndian)
{
Array.Reverse(guidBytes, 0, 4);
Array.Reverse(guidBytes, 4, 2);
}
break;
case SequentialGuidType.SequentialAtEnd:
Buffer.BlockCopy(randomBytes, 0, guidBytes, 0, 10);
Buffer.BlockCopy(timestampBytes, 2, guidBytes, 10, 6);
break;
}
return new Guid(guidBytes);
}
}
最终代码和演示项目可在 https://github.com/jhtodd/SequentialGuid[^] 找到
结论
正如我在开头提到的,通用的 COMB 方法已被各种框架大量使用,并且通用概念并不是特别新颖,当然也不是我原创的。我在这里的目标是说明该方法必须如何适应不同的数据库类型,以及提供强调定制方法必要性的基准信息。
稍加努力和适度的测试,就可以实现一种一致的生成顺序 GUID 的方法,该方法可以很容易地作为几乎任何数据库系统下的高性能主键使用。
历史
`v1` - 撰写!
`v2` - 采纳了 Rich Andersen 的建议,使用 DateTime.UtcNow 而不是 DateTime.Now,以提高性能并更新了代码格式。