需要唯一标识符:以IoT为例
让我们构建一个物联网应用程序,并在全球范围内部署了天气传感器。传感器将收集数据,我们将数据与传感器的ID一起存储。我们将运行多个数据库实例,传感器将写入地理上最接近的数据库。所有数据库都会定期交换数据,因此最终,所有数据库都将具有来自所有传感器的数据。
我们需要每个传感器具有全球唯一的ID。我们该如何实现?例如,我们可以运行分配传感器ID作为传感器安装过程的一部分的服务。这意味着其他建筑复杂性,但这是可行的。传感器ID是不可变的,因此每个传感器需要在安装后仅与ID服务进行一次交谈。那还不错。
如果我们需要为每个数据读数存储一个唯一的ID怎么办?每当我们需要存储数据时,就可以选择集中式ID服务。这会太强调ID服务太多,而当ID服务不可用时,传感器无法编写任何数据。
可能有什么解决方案?
在最简单的情况下,每个传感器都可以与远程ID服务交谈,并保留一个可以在本地分配的ID块,而无需进一步的协调。当它耗尽该块时,它会要求ID服务提供新的服务。此策略将减少ID服务的负载,即使ID服务暂时不可用,传感器也可以起作用。我们还可以生成本地阅读ID,并使用我们独特的不变传感器ID前缀。我们也可以很聪明,并使用诸如Flakeids之类的精美ID算法。
提到的策略旨在最大程度地减少协调的需求,同时仍然确保ID在全球范围内是独一无二的。目的是生成独特的ID,而无需任何协调。这就是我们所说的无协调独特ID。
uuid进入现场
翻转一枚硬币128次,每个头部写下1个,每个尾巴为0。这为您提供了128 1s和0s的序列,或128位随机性。这是一个足够大的空间,以至于产生相同序列的概率非常低,以至于您可以出于实际目的将其排除。
这与UUID有何关系?如果您曾经看过UUID,那么您就会知道它们看起来与此相似:420cd09a-4d56-4749-acc2-40b2e8aa8c42
。这种格式只是128位的文本表示。它是如何工作的? UUID字符串总共有36个字符。如果我们删除了4个破折号,这些破折号只是为了使其更加可读,那么我们将留下32个十六进制数字:0-F。每个数字代表4位和32 * 4位= 128位。因此,UUID是128位值。我们经常将它们表示为字符串,但这只是一个方便。
uuid已被明确设计为独特的,并且没有协调。当您有一个良好的随机发电机时,那么128个随机位就足以保证独特性。同时,128位并不多,因此存储时的UUID不会占用太多空间。
UUID版本
有多种版本的uuids。版本1-5在RFC 4122中定义,它们是最广泛使用的。版本6-8目前在draft status中,将来可能会获得批准。让我们简要介绍一下不同的版本。
版本1
版本1是通过使用MAC地址和时间作为输入来生成的。 MAC地址用于确保多台机器的独特性。时间用于确保同一机器上多个过程之间的唯一性。 Mac的使用意味着可以将生成的UUID跟踪到特定机器。这偶尔可能很有用,但是在其他情况下可能是不可取的,因为MAC地址可以视为私人信息。
有趣的是,时间部分不是基于通常的Unix时期,而是从1582年10月15日00:000:00.00以来使用100纳秒间隔。这是Gregorian calendar reform。有关标准Unix时期的UUID,请参见版本7。
版本2
版本2与版本1相似,但在UUID中添加了局部域ID。它没有被广泛使用。
版本3和5
这些版本使用哈希函数来生成UUID。哈希功能带有名称空间UUID和名称。名称空间UUID用于确保跨多个名称空间的唯一性。该名称用于确保命名空间内的唯一性。
版本3使用MD5作为哈希功能,而版本5使用SHA-1。 sha-1产生160位,因此摘要被截断为128位。
版本4
版本4 UUID可能是最受欢迎的版本。它仅依靠随机生成器来生成UUID,它类似于上面的硬币翻转示例。这意味着quality of the random generator至关重要。
版本6
版本6与版本1相似,但具有不同的字节顺序。它编码从最显着的位到最低显着的时间。这允许通过仅分类代表UUID的字节来正确排序UUID。
版本7
版本7使用48位时间戳和随机数据。与版本1、2或6不同,它使用毫秒的标准Unix时期。它还使用随机发电机而不是MAC地址。
版本8
版本8旨在用于实验和私人使用。
安全考虑
uuids设计为唯一,但并非设计为秘密。有什么不同?如果您生成一个UUID,则可以假设它与之前或之后生成的任何其他UUID不同,但是您不应将其视为密码或秘密会话标识符。这就是RFC 4122所说的:
不要以为很难猜测。例如,它们不应用作安全功能(仅仅拥有所有权授予访问权限的标识符)。可预测的随机数源将加剧情况。
QuestDB中的UUID
UUID是流行的合成ID,因为它们可以在没有任何协调的情况下生成,并且不会使用太多空间。 QuestDB用户经常存储UUID,但是直到最近,QuestDB还没有一流的支持。大多数用户将UUID存储在字符串列中。这是有道理的,因为正如我们在上面看到的那样,UUID具有规范的文本表示。
在字符串列中存储UUID是可能的,但效率低下。让我们做一些数学:我们已经知道每个UUID都有128位,那是16个字节。 UUID的规范文本表示有36个字符。 QuestDB使用UTF-16编码用于字符串,因此每个ASCII字符使用2个字节。每个字符串存储的固定成本也为4个字节。因此,需要36 * 2 + 4 = 76字节来存储一个仅包含16个字节信息的单个UUID!这不仅仅是浪费磁盘空间。 QuestDB在评估SQL谓词,连接表或计算聚合时必须读取这些字节。因此,将UUID存储为字符串也使您的查询较慢!
这就是为什么QUESTDB 6.7将UUID实现为头等数据类型的原因。这允许用户应用程序将列声明为UUID,然后每个存储的UUID都将仅使用16个字节。因此,SQL查询将更快。
演示时间
让S创建一个带有单个字符串列的表,并用10亿个随机uuid填充它。该列定义为字符串类型,因此UUID将被存储为字符串:
该演示创建的表格总数不到100 GB的磁盘空间。确保您有足够的磁盘空间。
您可能还需要通过query.timeout.sec
属性增加查询超时。有关更多详细信息,请参见Configuration。另外,您可以更改long_sequence()
函数以创建较小数量的行。
CREATE TABLE tab_s (s string);
INSERT INTO tab_s SELECT rnd_uuid4() FROM long_sequence(1000000000);
让我们尝试查询它:
SELECT * FROM tab_s WHERE s = 'ab632aba-be36-43e5-a4a0-4895e9cd3f0d';
它花费了约2.2秒。鉴于它在10亿个琴弦上进行了全表扫描,但我们可以做得更好,这并不糟糕!有多好?我看看。使用UUID列创建新表格:
CREATE TABLE tab_u (u uuid);
用第一个表中的uuid值填充它:
INSERT INTO tab_u SELECT * FROM tab_s;
新创建的表具有与第一表相同的值,但是列定义为UUID而不是字符串,因此它消除了我们上面讨论的浪费。让我看看谓词现在的表现:
SELECT * FROM tab_u WHERE u = 'ab632aba-be36-43e5-a4a0-4895e9cd3f0d';
此查询在我的测试框上大约需要380ms。这比原始2.2秒好几乎好6倍!速度是任何实时分析的关键,因此这当然很重要。
让我们检查磁盘空间。 du
命令显示每个表使用的空间。首先,带有字符串的桌子:
$ du -h
79G ./default
79G .
带有uuid的桌子:
$ du -h
15G ./default
15G .
将列声明为UUID保存了64GB的磁盘空间!
使用UUID优化查询性能,并且具有成本效益。最后但并非最不重要的一点是,uuID值的谓词在将来的QuestDB版本中会变得更快,因为我们正在考虑如何通过使用SIMD指令对它们进行矢量化!
!结论
我们使用UUID来生成全球唯一的ID,而无需任何协调。它们长128位,因此不会使用太多空间。这使它们非常适合分布式应用程序,物联网,加密货币或分散融资。
当您的应用程序存储UUID时,告诉您的数据库,它是一个UUID,请勿将它们存储在字符串列中。您将节省磁盘空间和CPU周期。