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

MeP*_*uck 3 database postgresql uuid cassandra

我需要将一个表从 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应用程序。

Bas*_*que 6

tl;博士

停止用 Cassandra 的术语思考。设计师在他们的设计中做出了一些有缺陷的决定。

  • 使用UUID作为标识符
  • 使用日期时间类型来跟踪时间。

? 不要将两者混合。

将两者混合是 Cassandra 的缺陷。

Cassandra 滥用 UUID

不幸的是,Cassandra 滥用了 UUID。你的困境表明他们的做法是不幸的愚蠢。

UUID 的目的是严格地生成标识符,而无需像序列号等其他方法那样需要与中央机构协调。

Cassandra 使用Version 1 UUIDs,它取当前时刻,加上一个任意的小数字,并与发布计算机的MAC 地址结合。所有这些数据构成了UUID 中128 位的大部分。

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

UUID 标准中有几个替代版本。这些替代方案不一定包含某个时刻。例如,第 4 版 UUID改为使用从加密强生成器生成的随机数。

如果要生成版本 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 版本可供选择。

要了解更多信息,请参阅:在 Postgres 中为 Insert 语句生成 UUID?

至于你的迁移,我建议“说实话”作为一种普遍的好方法。日期时间值应存储在具有适当标记名称的日期类型列中。标识符应存储在具有适当标记名称的适当类型(通常是整数类型或 UUID)的主键列中。

所以不要再玩 Cassandra 玩的那些愚蠢而聪明的游戏了。

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

Java 中用于表示时刻的等效类型是InstantorOffsetDateTimeZonedDateTime。JDBC 4.2 规范要求仅支持第二个,莫名其妙,而不是第一个或第三个。搜索 Stack Overflow 以获取更多此类 Java 和 JDBC 信息,因为它已被多次介绍。

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

将 UUID 存储为 CHAR

不,不要将 UUID 存储为文本。

TIMESTAMP 最合适,但我担心时区和冲突。

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

Postgres 总是TIMESTAMP WITH TIME ZONE以 UTC格式存储 a 。包含在提交值中的任何时区或偏移量信息都用于调整为 UTC,然后被丢弃。Java 检索这种类型的值作为 UTC。所以没问题。

当使用其他工具时会出现问题,这些工具具有善意但有严重缺陷的功能,即在生成文本以显示字段值的同时动态应用默认时区。从 Postgres 检索的值总是在 UCT 中,但它的表示可能已调整为另一个偏移量或区域。要么避免使用此类工具,要么确保将默认区域设置为 UTC 本身。所有程序员、DBA 和系统管理员都应该学习在工作中使用 UTC 工作和思考。

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

Java(旧版和现代版)和标准 SQL 中的日期时间类型表。

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

如果主机硬件时钟可以这样做,则版本 1 UUID 轨道和时间的分辨率可达 100 纳秒(1/10 微秒)。该java.time类捕获时间和微秒的分辨率(如Java 9及更高版本)。Postgres 以微秒的分辨率存储时刻。因此,使用 Java 和 Postgres,您将在这方面接近 Cassandra。

存储当前时刻。

OffsetDateTime odt = OffsetDateTime.now( ZoneOffset.UTC ) ;
myPreparedStatement.setObject( … , odt ) ;
Run Code Online (Sandbox Code Playgroud)

恢复。

OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;
Run Code Online (Sandbox Code Playgroud)

我可以达到 mirco/nano 秒的分辨率

你不能。今天的传统计算机时钟无法以纳秒为单位精确跟踪时间。

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

UUID 甚至 TimeUUID 没有必要总是增加

永远不能指望时钟总是在增加。时钟得到调整和重置。计算机硬件时钟不是那么准确。不了解计算机时钟的局限性是 Cassandra 设计的天真和不合理的方面之一。

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

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

如果时钟向后设置,或者可能已经向后设置(例如,当系统断电时),并且 UUID 生成器无法确定没有生成时间戳大于时钟设置值的 UUID,那么时钟序列必须改变。如果已知时钟序列的先前值,则可以将其递增;否则应设置为随机或高质量的伪随机值。

没有在UUID规范,承诺要“始终不断增加”。回到我的开场白,Cassandra 滥用 UUID。