跳到主要内容
Tooletto

为什么 UUID 几乎从不冲突(哪怕完全不做检查)

各种系统在完全没有中央协调的情况下不断生成随机 UUID,也没有做任何重复检查,然而它就是有效。生日悖论解释了为什么这其实是一个相当合理的赌注。

· 阅读需 1 分钟

对一个随机数寄予了出人意料的信任

一个标准的随机 UUID,是由任意数量完全没有协调的系统各自独立生成的——没有任何中央机构负责分发下一个可用的标识符,也没有任何针对已使用标识符列表的核对检查——尽管如此,在实际应用中,冲突基本上是一件从不会发生的事情。这是一个完全依靠概率、而不是依靠协调来支撑的、相当大胆的主张,要理解它为什么站得住脚,需要用到一段同样反直觉的数学知识,它也解释了一个更有名的谜题:一个房间里需要有多少人,才有可能出现两个人生日相同的情况。

简要说说生日悖论

经典的生日悖论问的是,一个房间里需要聚集多少人,才会有超过一半的概率出现两个人生日相同——答案是仅仅 23 人,对应 365 种可能的生日——这个答案让大多数人感到意外,他们的直觉通常认为这个数字应该更接近 365 本身。真正的答案之所以小得多,是因为相关的比较并不是"是否有某个具体的人和我生日相同",而是"房间里所有可能的配对中,是否有任何一对生日相同"——而可能的配对数量增长的速度,远快于人数增长的速度,因为仅仅 23 个人,就已经有 253 种不同的配对组合,每一种都是一次独立的、可能匹配的机会。这种规律——比较的机会数量以平方级增长,而项目数量只以线性增长——被称为生日问题,它直接适用于任何涉及从一个固定空间中抽取随机值并检查重复的场景,UUID 正是其中之一。

把同样的数学原理应用到一个 128 位随机标识符上

一个标准的随机 UUID(版本 4)从 122 个真正随机的比特位空间中抽取——总共 128 位中的少数几位,是被格式规范本身固定用来标识版本号的——这意味着可能取值的总数是 2 的 122 次方,一个近乎无法理解的巨大数字。把同样用于 365 天生日例子的生日悖论推理方式,套用到这个大得多的空间上,可以得出冲突开始变得可能的那个点:大约需要生成 2.7 万亿亿个 UUID,两两冲突的概率才会上升到大约 50% 左右——作为对比,这个数字比地球上所有海滩沙粒数量的估计值还要多。即使是单个系统,或者把大量系统加在一起,以任何现实世界中可能的速率生成 UUID,要接近这个门槛所需的时间,也会远远超过宇宙的年龄——这正是把 UUID 冲突当作实践中不需要检查的事情的真实数学依据。

为什么 v7 只改变了排序特性,完全不涉及这套数学原理

UUID 版本 7 是较晚才标准化的,专门用来生成能大致按时间顺序排序的标识符,它的实现方式是把毫秒精度的时间戳放在标识符的高位,而不是像版本 4 那样用随机数据填满整个值。这听起来可能会削弱冲突抵抗能力,因为现在值的一部分变得可预测而不是随机——但剩余的比特位依然填充着和之前一样多的真正随机数据,而且因为在同一毫秒内生成的两个 UUID,其随机的剩余部分仍然需要恰好匹配才会冲突,所以对于任何现实的生成速率来说,实际的冲突抵抗能力依然与版本 4 相当。v7 真正改变的,是一个完全不同、不相关的特性——排序方式——而不是这里讨论的冲突数学;这两者值得在概念上分开看待:唯一性来自随机空间本身的巨大规模,而可排序性来自时间戳在值中所处的位置。

这种对原始概率的信任,究竟换来了什么

正因为冲突概率是真正意义上、而不只是表面上可以忽略不计的,各个系统就可以完全独立地生成唯一标识符——一个离线工作的手机应用、一个几毫秒内启动又消失的无服务器函数、上千个并行运行的微服务实例——彼此之间完全不需要协调,也不需要一个中央注册表来追踪已经发放过哪些标识符,却依然可以有数学上站得住脚的预期,认定这些标识符中的任何两个都绝不会冲突。这正是一个由中央统一发放的顺序标识符,如果没有一个共享的协调机构就永远无法提供的特性,也正是随机 UUID 之所以成为分布式系统默认选择的全部原因——这些系统需要的标识符生成速度,是任何协调步骤都跟不上的。

相关工具

博客中的更多文章