支持关系操作的PostgreSQL中Cassandra的TimeUUID的替代

时间:2019-08-27 10:50:04

标签: database postgresql cassandra uuid

我需要将表从Cassandra迁移到PostgreSQL。

我需要迁移的内容:该表具有一个TimeUUID列,用于将时间存储为UUID。该列也用作聚类键。时间存储为UUID,以避免在同一毫秒内插入行时发生冲突。另外,此列涉及where子句,通常是timeUUID between 'foo' and 'bar',它产生了正确的结果。

我需要将其迁移到的位置:我要迁移到Postgres,因此需要找到合适的替代方法。 PostgreSQL具有UUID数据类型,但是据我到目前为止的阅读和尝试,它以4字节int形式存储它,但是在带关系运算符的where子句中使用时,UUID与String相似。

select * from table where timeUUID > 'foo'的结果中将有xyz

根据我的理解,UUID甚至TimeUUID不必总是增加。由于这种原因,与具有相同数据集的Cassandra相比,Postgres产生了错误的结果。

到目前为止,我已经考虑过:我考虑过将其存储为BIGINT,但是对于时间分辨率(以毫秒为单位),它很容易发生冲突。我可以解决mirco / nano秒的问题,但恐怕BIGINT会用尽它。

将UUID存储为CHAR可以防止冲突,但是我将失去在列上应用关系运算符的功能。

TIMESTAMP最适合,但我担心时区和碰撞。

我真正需要的是(tl; dr)

  1. 某些方法具有更高的时间分辨率或避免冲突(唯一值生成)。

  2. 该列应支持关系运算符,即 uuid_col < 'uuid_for_some_timestamp'

PS:这是一个Java应用程序。

2 个答案:

答案 0 :(得分:3)

tl; dr

停止用Cassandra术语思考。设计师在设计中做出了一些错误的决定。

  • UUID用作identifier
  • 使用日期时间类型来跟踪时间。

➥请勿将两者混用。

将两者融合是Cassandra的缺陷。

卡桑德拉滥用UUID

不幸的是,Cassandra滥用UUID。您的困境显示了他们的做法很不幸。

UUID的目的严格是生成标识符,而无需与其他方法(例如序列号)所需的中央机构进行协调。

Cassandra使用Version 1 UUIDs,它采用当前时刻加上任意小的数字,并与发行计算机的MAC address组合。所有这些数据将构成UUID中128 bits的大部分。

Cassandra做出了糟糕的设计决策,无法及时提取出该时刻用于时间跟踪,这违反了UUID设计的意图。 UUID从未打算用于时间跟踪。

UUID标准中有多个替代版本。这些替代方案不一定包含时间。例如,Version 4 UUIDs而是使用从加密强度较高的生成器生成的随机数。

如果要生成版本1 UUID,请安装通常与Postgres捆绑在一起的uuid-ossp插件(“扩展名”)(包装OSSP uuid库)。该插件提供了一些函数,您可以调用这些函数来生成UUID值。

  

[Postgres]将其存储为4字节int

Postgres将UUID定义为本机数据类型。因此,如何存储这些值实际上与我们无关,并且可能会在Postgres的未来版本(或其新的可插入存储方法)中发生变化。您传入一个UUID,然后您将获得一个UUID,这就是我们作为Postgres用户所知道的。另外,很高兴得知Postgres(以其当前的“堆”存储方法)将UUID值有效地存储为128位,而不是效率不高,例如,存储用于规范地显示UUID的十六进制字符串的文本对人类。

请注意,Postgres内置支持存储 UUID值,而不支持生成 UUID值。生成值:

  • 某些人使用pgcrypto扩展名(如果已安装在他们的数据库中)。该插件只能生成版本4几乎所有的UUID。
  • 我建议您改用uuid-ossp扩展名。这为您提供了多种UUID版本供您选择。

要了解更多信息,请参见:Generating a UUID in Postgres for Insert statement?

对于您的迁移,我建议将“讲真话”作为一般的好方法。日期时间值应存储在带有适当标记名称的日期类型列中。标识符应存储在具有适当标签名称的适当类型(通常为整数类型或UUID)的主键列中。

所以不要再玩卡桑德拉(Cassandra)玩的愚蠢而聪明的游戏了。

提取日期时间值,将其存储在日期时间列中。 Postgres具有出色的日期时间支持。具体来说,您需要将值存储在SQL标准类型TIMESTAMP WITH TIME ZONE的列中。此数据类型表示时刻,即时间轴上的特定点。

Java中表示时刻的等效类型为InstantOffsetDateTimeZonedDateTime。 JDBC 4.2规范仅要求对第二个(而不是第一个或第三个)的支持。在Stack Overflow上搜索有关此Java和JDBC信息的更多信息,因为已经有很多次了。

继续使用UUID,但仅将 用作Postgres中新表的指定主键列。您可以告诉Postgres自动生成这些值。

  

将UUID存储为CHAR

否,请勿将UUID存储为文本。

  

TIMESTAMP最适合,但我担心时区和碰撞。

TIMESTAMP WITH TIME ZONETIMESTAMP WITHOUT TIME ZONE之间有着天壤之别。所以永远不要只说时间戳。

Postgres始终在UTC中存储TIMESTAMP WITH TIME ZONE。提交值中包含的任何时区或偏移量信息都将用于调整为UTC,然后将其丢弃。 Java将此类型的值检索为UTC。所以没问题。

当使用其他工具时会出现问题,这些工具具有很好的意图但存在可悲的缺陷,可以在生成文本以显示字段值的同时动态应用默认时区。从Postgres检索的值在UCT中始终为 ,但其表示可能已调整为其他偏移量或区域。避免使用此类工具,或者确保将默认区域设置为UTC本身。所有程序员,DBA和系统管理员都应在工作中学习使用UTC进行工作和思考。

TIMESTAMP WITHOUT TIME ZONE完全不同。此类型缺少时区或从UTC偏移的上下文。因此,这种类型不能代表片刻。它具有日期和时间,仅此而已。这当然是模棱两可的。如果值是今年1月23日中午,我们不知道您是指东京中午,德黑兰中午还是托莱多中午-都是非常不同的时刻,相隔几个小时。等效 Java中的类型为LocalDateTime。搜索堆栈溢出以了解更多信息。

Table of date-time types in Java (both legacy and modern) and in standard SQL.

  

时间存储为UUID,以避免在同一毫秒内插入行时发生冲突。

第1版UUID跟踪和时间,如果主机硬件时钟可以做到,则其分辨率可以高达100纳秒(1/10微秒)。 java.time 类以微秒的分辨率捕获时间(从Java 9和更高版本开始)。 Postgres以微秒的分辨率存储时刻。因此,使用Java&Postgres,您将在这方面与Cassandra保持紧密联系。

存储当前时刻。

OffsetDateTime odt = OffsetDateTime.now( ZoneOffset.UTC ) ;
myPreparedStatement.setObject( … , odt ) ;

检索。

OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;
  

我可以解决mirco / nano秒的问题

不,你不能。如今,传统的计算机时钟无法精确跟踪纳秒级的时间。

并且仅将时间跟踪用作标识符值是一个有缺陷的想法。

  

UUID甚至TimeUUID不必总是增加

您可以从不指望时钟不断增加。时钟进行调整和重置。计算机硬件时钟不是那么准确。不了解计算机时钟的局限性是Cassandra设计的幼稚和不合理的方面之一。

这就是为什么版本1 UUID与当前时刻一起使用任意小的数字(称为clock sequence)的原因,因为当重置/调整时钟时,当前时刻可能会重复。负责任的UUID实现应注意时钟回落,然后递增该小数字以补偿并避免重复。根据RFC 4122第4.1.5节:

  

对于UUID版本1,时钟序列用于帮助避免在时钟倒退设置或节点ID更改时可能出现的重复。

     

如果时钟向后设置,或者可能已向后设置      (例如,在系统关闭电源的情况下),并且UUID生成器可以      不能确定没有生成大于UUID的时间戳      设置时钟的值,那么时钟序列必须      被改变。如果知道时钟序列的前一个值,则它      可以增加否则应将其设置为随机或      高质量的伪随机值。

UUID specifications中没有保证“一直在增长”的东西。回到我的开幕词,Cassandra滥用了UUID。

答案 1 :(得分:1)

听起来Cassandra TimeUUID是版本1 UUID,而Postgres生成版本4 UUID。您也可以在Postgres中生成V1:

https://www.postgresql.org/docs/11/uuid-ossp.html

我将pg_crypto用于UUID,但它只会生成V4。

其他人可以说更有权威性,但我记得Postgres中128位/ 16字节类型的UUID并不容易转换为数字。您可以将它们转换为文本,甚至是二进制字符串:

从foo中选择DECODE(REPLACE(id :: text,'-',''),'hex');

我无法想象这是一个超级快或好主意...

从您所说的内容出发,您遇到的问题是围绕时间戳记元素进行排序。我相信Ancoron Luciferis一直在研究这个问题。您可以在这里找到他的一些测试结果:

https://github.com/ancoron/pg-uuid-test

在Postgres中,序列“类型”是用于唯一序列号的标准功能。因此,您所说的是BIGSERIAL而不是BIGINT。时间戳列很大(也为8个字节),但不太适合唯一ID。在我们的设置中,我们将V4 UUID用于合成密钥,并将timestamptz字段用于时间戳。因此,我们有两列而不是一列。 (Postgres在这里是许多不同数据源的集中收集器,这就是为什么我们使用UUID而不是串行计数器BTW的原因。)就我个人而言,我喜欢使用时间戳的时间戳,因为它们更容易在不同的粒度级别上进行工作,推理和搜索。加!您可能会利用Postgres的惊人的 BRIN索引类型:

https://www.postgresql.fastware.com/blog/brin-indexes-what-are-they-and-how-do-you-use-them