这篇文章是我公开构建一个网络应用时的几篇文章之一。它被称为Lumeno,您可以在Twitter上关注其开发情况 - @LumenoDev,或者注册以便在它发布时接收通知。
在构建Lumeno的过程中,我已经花了一些时间来探讨如何安全地为数据库记录生成独特的主键。很多时候,作为开发者,你可能不需要考虑这个问题,但对于那些确实需要的人,这篇文章是为你而写的...
理性的声音
在深入这个主题的核心之前,值得指出的是,我看到很多开发者确实认为他们需要使用一个更复杂的系统来生成主键的唯一标识符。
如果你正在构建一个本质上是企业的系统,或者你熟悉并需要使用诸如集群、区域、副本等特性,那么这可能是对的。然而,大多数时候,开发者正在构建由单个服务器上的数据库驱动的适度大小的应用程序。
如果你是这样的开发者,那么生成唯一主键的默认方法(自增整数),应该能够满足你的需求。事实上,坚持使用它们有许多显著的好处…
自增整数
如果你至少对数据库有一定的了解,那么你会知道这是生成唯一键的标准方法。在幕后,系统维护一个内部计数器。当你插入一个记录时,计数器递增,并将新记录的主键设置为该值。
这是一个好方法吗?
简而言之,是的!虽然维护计数器会有一点开销,但在实际应用中可以忽略不计。好处是,您的记录将始终是唯一的,易于索引,且速度极快(实际上,总是最快的选项)。
我应该何时选择不同的方案?
我已经提到过一些情况,您可能会考虑其他方案,但在那些情况下,您也应当考虑是否可以坚持下去使用递增整数。有时候,只需调整您的模式以使用更大的数据类型(例如,BIGINT)就足够了。
即使调整不是那么微小,但在许多情况下,如果您正确设计应用程序和数据库,您仍然可以使用它们。
然而,如果您发现您需要做出太多妥协,您的应用程序变得不易管理,或者您的设置使得使用数据库计数器过于复杂,那么可能就是时候考虑其他方案了……
选择项... 挑选难如迷宫
好了,您已经决定自动递增整数不适合您。您可以用什么代替呢?好吧,如果您查阅了数据库手册,可能不会得到太多帮助。数据库提供了一个默认方法。如果您不想使用它,那么您基本上只能靠自己。所以,您就去互联网上搜索……
恭喜您!您发现了无数种生成唯一字符字符串的方法用作主键,而且您离开时对哪种方法清晰不明。幸运的是,至少在数据库方面,选择主要取决于四个因素
唯一性
生成关键的方法都有可能引起冲突(两个生成的键相同)。因此,您希望自己选择的方法冲突风险尽可能低。
字典顺序。
这是一个听起来很复杂但基本上指的是一种组织方式。在数据库中,我们指的是能够使用键来排序我们的记录。
效率
根据您选择的方法,您的数据库可能已经包括了生成关键字的函数。但是,即使它提供了,也最好在应用程序中生成它,这样数据库的工作量就会减少。请注意,这种方法有一些权衡,例如无法执行INSERT INTO SELECT等操作。
存储
从键的长度,到它构成的内容,到如何在数据库中存储它(例如,整数,字符串,作为二进制等)。
现在我们知道了该寻找什么,让我们来看看一些选项。我只会涵盖几个,因为(如您通过研究发现一样)您只能有如此多的方式来生成字符字符串。换句话说,许多不同的算法实际上只是主题的变体。
UUIDs,即金标准
这些是独特键的鼻祖。该缩写代表通用唯一标识符,坦白说,这是非常荒谬的。我们怎么可能知道它在整个宇宙中都是唯一的呢?微软决定称其为GUID,即全球唯一标识符。这更有意义,但无论如何,UUID这个术语在微软(及其产品)之外的地方更为通行。
那么它是怎么回事呢?
UUID是一个128位的字母数字字符字符串,由破折号分隔以使其可读。128位等于32个字符,因此它看起来是这样的
123e4567-e89b-12d3-a456-426614174000
很简单,是吗?其实不是。实际上有五种不同的方式可以创建UUID。真的吗?但等等,还有更精彩的!
存在版本6、7和8的草案提案。如果您在阅读本文时在想,“怎么可能需要8种不同的方式来创建字符串?”,那么我肯定与您有同感。
我不会进入生成版本所使用的不同算法。我只想说的是,如果你想要一个真正的随机UUID,那么你需要使用V4版本。
我该如何创建它们呢?
几乎所有你能想到的编程语言都有无数可用的库。更进一步,许多数据库包括用于生成它们的本地函数(但是你需要检查它们生成哪些版本)。Laravel 包括 Str::uuid() 和 Str::orderedUuid() 函数。
ULIDS
这代表通用唯一字母顺序可排序标识符。它的目标是通过提供更短、可排序的键来改进当前UUID格式。
ULIDS长度为26个字符,并使用包含开始时间的格式(这使得它们可排序)。一个ULID看起来像这样
01ARZ3NDEKTSV4RRFFQ69G5FAV
ULIDs还包括一些其他特性,使其比UUID更有吸引力,例如大小写不敏感、base32等。
我该如何创建它们呢?
包含该格式规范的Github仓库也维护了不同编程语言的实现列表(默认是JavaScript,这使得它非常适合许多应用)。
雪花ID
这种生成键的方式是由Twitter为他们的系统开发的,后来被其他大科技公司采纳。
与UUID和ULIDs不同,雪花ID仅由数字组成。此外,它们只有16个字符长,是其中最短的。一个雪花看起来像这样
3398978255192064
雪花标识符也有一个好处,就是它们以时间戳开始,这使得它们可排序。
我该如何创建它们呢?
Twitter已经存档了包含其原始实现源代码的开源仓库。它似乎现在已经合并到了他们的Twitter服务器项目中。然而,对于大多数主要的编程语言,都存在许多独立的实现。
我们应该使用哪个?
简短的回答是,这取决于。每种方法都有它的优点,无论是易用性、库可用性、整体熟悉度、性能、效率,还是需要与类似格式的数据一起工作等。
由于没有“正确”的做法,我将解释我选择了哪一个,以及原因。为了做到这一点,我会回到我在文章中早期写的清单。
唯一性
这三种实现都声称很好地处理了这种情况,它们都能在千分之一秒内生成数千个键,而不会有碰撞。所以实际上,这不是选择其中一个而不是另一个的真正的因素。
当谈到可以创建的最大键数时,有一些限制,但这些限制在遥远的未来,例如2060年或更晚,并且现在建立的系统仍然运行或至少在过去几年中已经发生了显著改变的可能性很小。
字典序
某些UUID实现是可排序的(版本1和2),但是因为这些版本依赖于MAC地址,它们往往不会使用,因为可能存在安全风险。你也会发现版本4的不同变体,这些变体改变了键的顺序,使得时间戳排在前面,这使得它可排序,但是已经有人对碰撞/随机性提出了担忧。
ULIDs始终被设计为从时间戳开始可排序。雪花也是。
效率
这一点应该是相当明显的。由于UUID的长度最长,它们的生成速度最慢,然后是ULIDs,然后是雪花。我做一些基础的基准测试来看一下差异,结果相当显著。它们可能看起来不多,但在规模上,当你每秒需要生成数千条记录时,这确实会累积起来。
Standard UUID v4 - 5.6 milliseconds
Sortable UUID v4 - 8.3 milliseconds
ULID - 3.1 milliseconds
Snowflake - 0.4 milliseconds
此外,由于主键也将作为外键,如果在连接查询中使用它们,那么使用整数而不是字符串将显著提高查询速度。简单地说,在所有指标和任何场景下,整数都比字符串要好。
存储
与效率一样,UUID 占用空间最大,其次是 ULID,然后是 Snowflake。也应注意的是,字符串索引比整数索引占用更多空间且效率较低。
例如,一个包含100,000条记录的索引,涉及整数主键的大小大约是几兆字节,但对于字符串,它可能增加到几百。你可以通过将字符串转换为二进制来提高效率,但这样你的应用程序就必须处理哪些转换到和从二进制,这很麻烦。
最后,我设置了一个包含100,000个用户和几个附属表中的几百万条记录的测试数据库。使用 Snowflake ID,数据库大小约为2.7 GB。当使用字符串(UUID 或 ULID)时,约为6.2 GB。这看起来可能并不多,但数据库越小,可以放入内存的就越多,从而获得更好的性能。
结论
在这三种选择中,Snowflake 是最明显的赢家。首先,它是最短的,这意味着它使用的空间更少。它还以时间戳开头,这使得它可以排序。最后,由于它是整数,它可以更快地索引,并且使用它在连接查询中也将运行得更快。
另一个附加的好处是,尽管 Snowflake 使用整数,但它不使用简单的自增,这意味着无法猜测数据库表中有多少记录。虽然这是一种通过隐藏来保证安全性的做法,但它确实是一块很不错的蛋糕。
结束语
希望您喜欢这篇文章,并从中发现了有趣的东西。如果您想看到更多来自我的内容,那么您可以关注我 Twitter。
如果您想了解 Lumeno 启动时的信息,那么为什么不订阅以便在启动时收到通知。
再次感谢,祝您有个美好的一天!
driesvints, rsmsp, achmedislamic, phcostabh, sshead 喜欢了这篇文章