UUIDv7 并没有你认为的特性
建立在硅片碎片上,我将称之为。UUIDv7 很奇怪。关于 UUIDv7 的大多数博客文章都宣扬这些好处:单调递增的 ID,一个有用的对象创建时间戳;以及不可猜测的 ID。但这些特性并不严格保持。某些符合 RFC 的 UUIDv7 实现极大地违反了这些特性。UUIDv7 似乎严格提供的唯一有用特性是唯一性,因此具有碰撞抗性。我已经讨论过 UUIDv7 ID 是可以被猜测的,因此这篇文章探讨其他特性。时间戳有用吗?表面上,似乎在 ID 中嵌入时间戳可以消除在单独的数据库列中跟踪创建时间的需要,从而提供稍微更高效的数据库操作。如果你需要按照创建时间经常列出事物,如果主索引提供该行为,就很有用,消除了另一个索引的需要。不幸的是,嵌入在 UUIDv7 ID 中的时间戳可能与对象创建时间无关。Postgres 文档中的 uuid_extract_timestamp 友好地警告说,“提取的时间戳不一定与生成 UUID 的时间完全相等”。RFC 9562 允许实现修改时间戳字段,使用非严格来源于系统时钟的值。这与 UUID 单调性和时间戳准确性之间的紧张关系有关。在某些场景中,时间戳与当前时间之间的差异可以非常大。实现可能会改变实际时间戳。— RFC 9562 §6.1 此规范对时钟值与实际时间之间的接近程度没有任何要求或保证。— RFC 9562 §6.1 …实现可能会提前增加时间戳… — RFC 9562 §6.2 提前增加时间戳是一个有趣的问题。如果生成器的负载过大,以至于计数字段经常溢出,那么时间戳可能会加速到未来。使用足够大的计数器是明智的,RFC 说最小大小应该是 12 位。计数器溢出将需要非常激进的创建速率,每个节点每毫秒超过 4,096 个 ID,这超出了许多系统的限制。然而,当时间戳冻结时,这种溢出变得相关;我将在后面的部分讨论它。示例:Python uuid.uuid7() 一个显著的实现是在 Python 标准库中的 UUIDv7。该增加只在某些情况下发生,例如当计数器溢出时。不幸的是,如果时钟向后跳动,生成器可能会陷入增加循环,时间戳可能会比系统时间快得多。这是一个简单的示例: import uuid from datetime import datetime # 用当前时间初始化生成器 t = uuid.uuid7().time now = datetime.now() delta = (datetime.fromtimestamp(t/1000) - now).total_seconds() print("初始差异: ", delta) # 请用户更改时间 input("将时钟向后调一小时") while True: for _ in range(100000): uuid.uuid7() t = uuid.uuid7().time now = datetime.now() delta = (datetime.fromtimestamp(t/1000) - now).total_seconds() print("新差异: ", delta) 这将给出: $ uv run --python 3.14 main.py 初始差异: -0.000748 将时钟向后调一小时 新差异: 3695.277581 新差异: 3795.042411 新差异: 3894.80756 新差异: 3994.573106 ^C 我们期望差异为 3600 秒(一个小时),因为该实现拒绝允许嵌入的时间戳向后跳跃,并且我们期望差异会随着系统时钟赶上冻结的时间戳而减小。但是,当负载超过每毫秒一个 ID 时,差异将无限增加。在较低的负载下,系统时钟赶上时间戳的时间会更长,但我们没有看到失控的效果。相关问题显示 Python 开发者推理这是与 RFC 一致的,是与其他实现一致的,并且在一个“有缺陷的系统”上我们“无能为力”。我认为这些观点是正确的,因为我在其他实现中看到了相同的行为。然而,如果现有的 UUIDv7 实现能够处理系统时钟跳跃而不失控,将会很有帮助。在时间敏感环境中,存在许多真实世界的时钟跳跃的例子,甚至几十年不同步。泄漏时间戳有害吗?在 ID 中嵌入时间戳意味着你无法分享 ID 而不分享时间戳。很难想到一个真正重要的例子。一个例子可能是一个用户个人资料页面,该页面没有披露帐户创建时间,但 URL 中的 UUIDv7 ID 泄漏了高精度时间戳。这个问题通常在我的安全关注列表中非常低,因为我在进行安全评估时发现了更严重的问题。
阅读全文 →本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡