它们为何存在

随机的 UUIDv4 在唯一性上极佳,在有序性上糟糕,而那种无序会悄然损害数据库性能(数据库键一文讲述了这个故事)。在 UUIDv7 把一个时间有序的 UUID 标准化之前,若干项目发明了各自的标识符格式以求得同样的好处:一个按创建时间排序、紧凑且对 URL 友好、却仍无需中央协调的 ID。它们值得了解,因为你会在真实系统中遇到它们,也因为它们例示了同样的那一小撮取舍。

共同的配方

几乎每一种可排序 ID 都用一个想法:把时间戳放在前面,把随机或顺序位放在后面。于是把这些 ID 作为文本排序,就大致按时间把它们排序,因为最高有效的部分是时钟。把整体编码进一个保序的进制(使文本的排序方式与字节相同),便让这些 ID 作为纯字符串就可按字典序排序。各格式的不同在于:它们在时间与随机性上各花多少位、如何编码结果,以及是否需要生成器之间的任何协调。

ULID

是一个 128 位标识符,与 UUID 等宽,渲染为 Crockford base32 中的 26 个字符。前 48 位是一个毫秒级时间戳,其余 80 位是随机的。其编码不区分大小写,并被选成可读的,且可按字典序排序,还有一个可选的单调模式,即便对同一毫秒内创建的 ID 也保证排序。ULID 曾是对 v4 排序问题最流行的答案之一,又因其为 128 位,可存储在与 UUID 相同的空间中。

KSUID

KSUID(来自 Segment)是一个 160 位标识符,编码为 27 个 base62 字符。它使用一个秒级分辨率的 32 位时间戳并带有自定义纪元(使其时间戳在很多年内保持紧凑),其后是 128 位随机性。更大的随机部分使碰撞即便在高生成速率下也微乎其微,代价是比 UUID 更宽。与 ULID 一样,作为文本排序时它按时间排序。

Snowflake

Snowflake(源自 Twitter)是另一种形状:一个 64 位整数,而非一个 128 位值。它把一个毫秒级时间戳、一个机器或数据中心标识符,以及一个每毫秒的序列号打包进 64 位,于是它能装进一个标准的有符号 bigint 列。那种紧凑是它对极大数据集的吸引力所在。难处在于协调:每个生成器都需要一个唯一的机器 ID,须加以分配和管理,因此 Snowflake 用一个小得多的标识符换取了 UUID 那种无需协调的性质。许多系统都有自己仿 Snowflake 的方案,建立在同样的打包想法之上。

NanoID

NanoID 是个例外:它根本不基于时间。它只是一个紧凑、URL 安全的随机字符串,字母表和长度可配置,被设计为随机 UUID 的一个更小、更友好的替代品,适用于你想要在一个短字符串中有一个不透明标识符之时。把它与 UUIDv4 而非与可排序格式相比:它优化的是大小与 URL 友好,而非排序。

它们如何相比

这些取舍沿着几条轴排开。大小:Snowflake 为 64 位,ULID 和 UUID 为 128 位,KSUID 为 160 位。编码:base32(ULID)、base62(KSUID)、一个整数(Snowflake)或十六进制(UUID)。协调:只有 Snowflake 需要分配机器 ID;其余都无需协调。可排序性:除 NanoID 外,全都按时间有序。还有一条共同的告诫:任何以时间戳为前缀的 ID 都会把它的创建时间暴露给任何能读取它的人,这对大多数用途无妨,但当一个标识符被公开暴露时值得注意。

UUIDv7 为何改变了权衡

这份清单不如从前重要的原因,是 UUIDv7(见版本一文)把同样的时间戳加随机性配方标准化进了常规的 128 位 UUID 格式之中。它给你时间排序与良好的索引局部性,同时仍是一个每个数据库和库都已理解的标准 UUID,无须自定义编码、无须协调。所以对于新系统,诚实的默认是先选用 v7,仅当你需要某种替代品的特定性质时才用它:一个能装进 bigint 的 64 位宽度(Snowflake)、某种特定的文本编码,或一个更大的随机空间。它们解决了一个真实的问题;v7 如今以一种标准形式解决了其中大部分。