UUID 真的不会冲突吗——由错误的运维与实现所招致的重复模式
· 小村 豪 · UUID, 标识符, 分布式系统, 数据设计, 实现
把 UUID 用作主键,结果有一天却报出 duplicate key。
这一瞬间,很大概率会引出「UUID 到底还是会冲突吧」这样的说法。
不过,实务中出现的 UUID 重复,与其说是 UUID 这个规范本身的问题,很多情况下更是 实现或运维破坏了规范所依赖的生成前提。RFC 9562 中,UUIDv4 拥有 122 位的随机区域,UUIDv7 也是在时间戳以外的 74 位中,以使用随机数或计数器来保证唯一性为前提定义的。而 UUIDv8 则被明确写为「与实现相关,不应假设其唯一性」。123
另外,Python 标准库中也说明 uuid4() 是以密码学安全的方式生成的,至少在「正常使用规范的实现」这一前提下,UUID 一侧的保障相当可靠。4
本文将整理 因错误的运维或实现而导致 UUID 冲突 的典型模式,并配合相应的预防措施。 内容以 截至 2026 年 3 月 可确认的 RFC 9562、Python 官方文档、PostgreSQL 官方文档为依据。546
1. 先说结论
先简要总结一下,危险的正是这些模式。
| 模式 | 会发生什么 | 首先应采取的对策 |
|---|---|---|
| 用固定 seed 或较弱的 PRNG 自行拼装类似 UUIDv4 的值 | 在不同进程或不同节点上重现出相同的序列 | 使用操作系统 / 运行时标准提供的 UUID API |
| 在 fork、VM 快照、容器复制之后原样继承生成状态 | 随机数或计数器的状态被回退,产生重复 | 重新审视 fork 后的重新播种、克隆后的重新初始化、持久状态的处理方式 |
| 把 UUIDv3 / v5 误解为「每次都是新 ID」来使用 | 相同 namespace 与相同 name 会重新生成相同的 UUID | 理解它是确定性 ID,并限定使用场景 |
| 自行实现 UUIDv1 / v6 / v7 / v8,却对 clock rollback 或 node/counter 处理得很随意 | 在高频生成或多节点场景下更容易发生重复 | 使用现成的库,减少自行实现的生成器 |
| 把 UUID 在中途截断,或压缩成其他格式 | 自行放弃了原本 128 位所保证的唯一性 | 保存与比较都应使用完整长度 |
| 数据库侧没有设置 UNIQUE / PRIMARY KEY | 重复悄悄混入,导致原因排查滞后 | 在存储层持有唯一约束 |
总而言之,与其说是 UUID 发生了冲突,不如说更多情况是 在中途的设计中,削减了本应期待 UUID 具备的唯一性。
2. 首先该怀疑的不是「UUID 的数学」,而是「生成与运维」
UUID 的讨论之所以容易变得复杂,是因为不同 version 的性质各不相同。
- UUIDv4 以随机数为基础。RFC 9562 规定,除 version / variant 之外的 122 位由随机数填充。1
- UUIDv7 具有便于按时间排序的结构,除了 Unix 毫秒时间戳之外,剩余部分由随机数或经过精心设计的种子的计数器构成。2
- UUIDv3 / v5 是基于名称(name-based)的。相同 namespace 与相同 canonical name 生成相同 UUID,才是正确的行为。7
- UUIDv8 是用于实验或厂商自定义的,其唯一性取决于实现。RFC 中写明「不应假设其具有唯一性」。3
也就是说,即便都说自己「在使用 UUID」,其内部实际上到底是
- 标准库的
uuid4() - 自行实现的
timestamp + random uuid5(namespace, name)- 外观上模仿 UUIDv8 的自定义格式
结论会完全不同。
flowchart TD
A[发现 UUID 重复] --> B{真正是在哪里产生了相同的值}
B --> C[生成器较弱]
B --> D[状态被回退]
B --> E[误用了基于名称的 UUID]
B --> F[保存时被截断]
B --> G[数据库侧没有唯一约束]
C --> H[实现失误]
D --> H
E --> H
F --> H
G --> H
在实务中,从这张图的右侧开始排查会更快。
3. 模式一:号称是 UUIDv4,实际却使用了较弱的 PRNG
最常见的就是这一种。
- 用类似
Math.random()的通用 PRNG 拼出 128 位 - 在启动时用
time()或 PID 作为 seed - 自行组装出「看起来像 UUID 格式的 32 位十六进制数」
外观上看是 UUID,但 只要随机数源较弱,相同的序列就可能在不同进程或不同节点上重现出来。
RFC 9562 指出,为了同时保证 UUID 的唯一性与不可预测性,应该使用 CSPRNG。这是一项推荐性要求(SHOULD),根据用途也可能存在例外设计,但如果用通用 PRNG 自行拼装 UUID,就应该能说明其理由。此外,RFC 还写明,在 process fork 这类状态变化发生时,应当适当地重新播种 CSPRNG 状态。8
Python 的 uuid.uuid4() 也说明它是以密码学安全的方式生成随机 UUID 的。4
在这里,实务上的结论很简单。
- 不要自行生成 UUID
- 不要手动干预随机数种子
- 直接使用标准库,或广泛使用的现成实现
如果因为「比较轻量」「一直都是这么用的」就继续维持自定义生成器,日后代价往往最为高昂。
4. 模式二:在 fork、快照、克隆时使生成状态被回退
第二危险的是 生成器状态被复制或回退的运维方式。
RFC 9562 明确建议在 fork 之后重新播种,并说明没有稳定存储(stable storage)的实现,clock sequence、counter、随机数据的生成频率会增加,重复概率也会随之上升。89
由此自然可以推出一些实务上的注意点。
- 获取 VM 快照后,用同一份镜像还原出多个实例
- 容器镜像启动时,自定义生成器从相同的初始状态启动
- worker fork 之后,共享了 PRNG 状态或计数器状态
在这类运维方式下,UUID 的生成序列可能会在不经意间被重现。 RFC 并没有直接写明「快照本身是危险的」,但从 fork 后重新播种以及生成器状态处理的注意事项中,可以推导出这个相当实用的结论。89
对策大致有以下这些。
- 不要长时间持有自定义的 UUID 生成状态
- 在 fork / clone / restore 之后立即重新初始化
- 尽可能改用每次都从操作系统获取随机数的实现
- 如果是高频生成器,就把状态管理与重新播种的规范明确写下来
5. 模式三:把 UUIDv3 / v5 误解为「每次都是新 ID」
UUIDv3 / v5 并不是不容易发生冲突的随机 ID。 它是 能够从相同名称重新生成相同 ID 的确定性 ID。
RFC 9562 中写明,由相同 namespace 中相同 canonical format 的相同 name 所生成的 UUID 必须相等。7 因此,如果采用下面这种用法,出现重复并不是事故,而是符合规范的正常行为。
- 把
uuid5(NAMESPACE_URL, "https://example.com/users/42")每次都当作「新建编号」使用 - 不把 tenant 纳入 namespace,而用全体客户共通的 namespace + email 来发号
- 认为即便同一个逻辑名称在每次重试时重新发号,也会得到不同的 ID
反过来说,如果 name 的规范化(canonicalization)存在偏差,同一个对象也会得到不同的 UUID。RFC 也反复强调了 canonical representation 处理方式的重要性。710
在这一系列问题中,重要的是以下 3 点。
- UUIDv3 / v5 不是「不会重复的编号」,而是「相同输入则相同 ID」
- 不要让 namespace 的设计含糊不清
- 把 name 的规范化规则写成规范
6. 模式四:自行实现基于时刻的 UUID 或 UUIDv8
UUIDv1 / v6 / v7 / v8,如果只是模仿外观,会相当危险。
6.1 对 UUIDv1 / v6 中的 node 或 clock sequence 处理得很随意
RFC 9562 中,UUIDv6 是为改善数据库局部性(locality)而对 UUIDv1 重新排序得到的版本,涉及 clock sequence 与 node。此外,关于分布式环境下的节点冲突抵御能力(node collision resistance)与状态保持,也有多处注意事项。11912
而且 RFC 甚至写明,由于虚拟机与容器的出现,MAC 地址的唯一性已不再能被保证。5
因此,下面这些设计是危险的。
- 想当然地认为「因为是 MAC 地址所以是唯一的」
- 通过镜像预置的方式复制 node ID
- 每次重启都把 clock sequence 固定回同一个值
6.2 自行实现 UUIDv7 却放任 counter rollover 或 clock rollback
UUIDv7 相当实用,但 RFC 对高频生成时的单调性(monotonicity)与计数器处理写得很细致。它还明确指出,不应在 clock rollback 或 counter rollover 时明知会产生重复却仍然返回。213
也就是说,下面这些实现是危险的。
- 在同一毫秒内需要大量发号,却没有相应的计数器设计
- 时间发生回退时什么也不做,继续生成下去
- 多个进程各自单独初始化同一个内部计数器
6.3 把 UUIDv8 当作「新的 UUID 规范」这种轻松心态来使用
UUIDv8 看起来很方便,但 RFC 9562 说得相当明确:UUIDv8 的唯一性取决于实现,不应假设其具有唯一性。3
因此,下面这种「自家专属 UUID」
- 嵌入 timestamp
- 嵌入 shard id
- 嵌入某种业务含义
- 剩余部分随意填入 random
其设计文档本身,就等同于 UUID 的唯一性规范本身。 未经评审直接引入是相当危险的。
7. 模式五:把 UUID 在中途截短
即便生成阶段是正确的,也有可能在保存或比较阶段被破坏。
典型例子包括:
- 只用开头 8 个字符来代替外键
- 把 128 位的 UUID 压缩成 64 位整数
- 字符串列长度不够,导致末尾被截断
- 把日志或界面显示用的缩略表示,直接当作唯一键使用
这里重要的是,改变表示形式本身并不是坏事。
- 去掉连字符
- 统一大小写
- 用 16 字节的二进制形式保存
像这类 不损失 128 位信息的转换 是没有问题的。 危险的是 削减唯一性来源本身的转换。
尤其是为了方便查看而单独创建的「简短 ID」,如果不知不觉之间比原本的 UUID 更被优先使用,就很容易演变成事故。
8. 模式六:数据库侧没有唯一约束
这一点尤其重要。
即便 UUID 已经足够难以冲突,如果真的不能容忍重复,就应该在存储层也持有唯一约束。
PostgreSQL 的官方文档中说明,唯一约束(unique constraint)能保证某一列或某组列的值在整张表中是唯一的,而主键(primary key)则是 同时具备唯一且非空(unique and not null) 的行标识符。6
RFC 9562 也指出,UUID 在实现上能够提供足够的唯一性,但 无法绝对保证真正的全局唯一性(global uniqueness)。并且,在冲突影响较大的用途中,应采取更强的对策。14
在实务中,基本会采用如下组合。
- 把 UUID 用作不易冲突的 ID
- 数据库通过 UNIQUE / PRIMARY KEY 来持有最后一道防线
- 设计重复发生时的重试、幂等性(idempotency)与事件记录
使用 UUID,与不设置唯一约束,这两者并不等同。
9. 面向实务的检查清单
最后,整理成一份便于在引入或审计时直接使用的清单。
- 确认是否在自行生成 UUID
如果能改用
uuid4()/uuid7()这类标准 API,首先就应该改用它们。 - 把 UUID 的 version 作为规范确定下来 明确写明 v4/v7 是随机系列、v3/v5 是确定性系列、v8 是自定义规范。
- 梳理 seed 与生成器状态的处理方式 确保在 fork、worker 重启、快照、克隆之后,不会继承相同的状态。
- 确认保存时是否保持了完整长度 不要把前缀比较或缩略显示当作本来的键来使用。
- 在数据库中设置 UNIQUE / PRIMARY KEY UUID 是降低概率的机制,本身并不等同于约束。
- 让重复能够被观测到 不要吞掉 duplicate key 错误,要能追溯到是哪个 generator / node / deployment 产生的。
10. 总结
UUID 的冲突事故,大多不是因为 UUID 本身较弱,而是从 实现或运维破坏了 UUID 的前提 开始的。
- 用较弱的随机数自行拼装
- 在 fork 或快照之后使状态被回退
- 把基于名称的 UUID 用于编号场景
- 随意自行实现 v7 或 v8
- 在中途截短,放弃了唯一性
- 去掉了数据库侧的唯一约束
一旦做了这些事,就与 自己主动创造出容易冲突的状况 几乎没有区别。
发现重复时,首先应该怀疑的不是 UUID 的数学原理,而是 生成器、状态管理、保存格式、约束设计。 按这个顺序排查,通常都能相当有效地锁定原因。
11. 相关文章
12. 参考资料
-
IETF RFC 9562, Section 5.4 UUID Version 4. 关于 UUIDv4 的 122 位随机区域。 ↩ ↩2
-
IETF RFC 9562, Section 5.7 UUID Version 7. 关于 UUIDv7 的时间戳、随机位、计数器的设计思路。 ↩ ↩2 ↩3
-
IETF RFC 9562, Section 5.8 UUID Version 8. 关于 UUIDv8 的唯一性取决于实现,不应假设其具有唯一性。 ↩ ↩2 ↩3
-
Python 3.14 文档,
uuid模块. 关于uuid4()的密码学安全生成方式、uuid5()的确定性行为,以及uuid7()/uuid8()的性质。 ↩ ↩2 ↩3 -
IETF RFC 9562, Universally Unique IDentifiers (UUIDs). UUID 的格式、各 version、最佳实践的基准文档。 ↩ ↩2
-
PostgreSQL 文档, Constraints. 关于通过 UNIQUE 约束与 PRIMARY KEY 来保证唯一性。 ↩ ↩2
-
IETF RFC 9562, Section 6.5 Name-Based UUID Generation. 关于相同 namespace + 相同 name 会生成相同 UUID,以及规范化(canonicalization)的重要性。 ↩ ↩2 ↩3
-
IETF RFC 9562, Section 6.9 Unguessability. 关于使用 CSPRNG,以及 fork 后重新播种的做法。 ↩ ↩2 ↩3
-
IETF RFC 9562, Section 6.3 UUID Generator States. 关于稳定存储(stable storage)与生成器状态的处理方式。 ↩ ↩2 ↩3
-
IETF RFC 9562, Section 5.5 UUID Version 5. 关于基于 namespace + canonical name 的 name-based UUID 规范。 ↩
-
IETF RFC 9562, Section 5.6 UUID Version 6. 关于 UUIDv6 的 node / clock sequence / 数据库局部性。 ↩
-
IETF RFC 9562, Section 6.4 Distributed UUID Generation. 关于分布式环境下的节点冲突抵御能力。 ↩
-
IETF RFC 9562, Section 6.2 Monotonicity and Counters. 关于 clock rollback、counter rollover、批量生成时的注意事项。 ↩
-
IETF RFC 9562, Sections 6.7 and 6.8. 关于冲突抵御能力(collision resistance)与全局唯一性的考量方式。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
用 PerfView 与 dotnet-trace 定位“变慢”的原因 ── .NET 性能排查实务入门
当业务应用出现“变慢”“CPU 占满”“偶尔卡死”时,该用哪个工具看什么?本文梳理 PerfView 与 dotnet-trace 的分工、CPU 采样的解读方法(inclusive/exclusive)、用 ThreadTime 排查阻塞时间,以及与 EventSourc...
面向 BtoB 企业的 Google 广告小预算运营 ── 用每月数万日元做出成果的设计与每周操作流程
本文面向希望以每月数万日元预算启动 Google 广告的 BtoB 企业,是一份实务指南。内容整理了平均每日预算与结算上限的运作原理、转化跟踪的搭建、聚焦高意向关键词、否定关键词与搜索字词报告的每周运营方法,以及小预算下的出价策略选择,均以 Google 官方资料为依据。
打造能被地区名搜索到的网站 ── 中小企业的本地SEO实战指南(地区页面与Google商家资料)
用「地区名称+行业」搜索却找不到自家公司的中小企业,适合看这篇。内容涵盖修正本地SEO的先后顺序:整理Google商家资料、统一NAP信息、设计地区页面、网站端的基础建设,以及在Search Console上衡量效果,全部以Google官方信息为依据整理成实务步骤。
Windows 事件日志・ETW 入门 ── 让业务应用程序的日志接入操作系统标准机制
Windows 业务应用程序的日志,是不是只靠文件日志来解决?事件日志与 ETW,是运维人员和操作系统标准工具所能看到的另一层记录。本文整理三种手段的分工方式、.NET 中的写入方法、通过 EventSource 进行的 ETW 埋点、收集与排查的实务做法,以及来源未注册、...
不只是 appsettings.json ── Windows 业务应用的配置管理实务(环境差异化设置・敏感信息・写入位置)
本文从实务角度整理 Windows 业务应用的配置管理,涵盖 appsettings.json 的分层、IConfiguration 与 IOptions/IOptionsSnapshot/IOptionsMonitor 的选用方式、环境差异化设置、可写入配置的存放位置、敏...
常见问题
汇总了咨询这一主题时常见的问题。
- UUID 不会冲突吗?
- 只要正常使用就足够难以冲突。RFC 9562 中,UUIDv4 拥有 122 位的随机区域,UUIDv7 也是在时间戳以外的 74 位中使用随机数或计数器来保证唯一性这一前提下定义的。像 Python 的 uuid4() 这样,只要正常使用规范的实现,这个前提就相当可靠。不过,RFC 9562 自身也指出,UUID 无法绝对保证真正的全局唯一性(global uniqueness),在冲突影响较大的用途中应采取更强的对策。正因如此,数据库层面的唯一约束才是最后一道防线。
- UUID 的重复为什么会发生?
- 实务中出现的 UUID 重复,大多不是规范本身的问题,而是实现或运维破坏了规范所依赖的生成前提所导致的。典型模式共有 6 种:用固定 seed 或较弱的 PRNG 自行拼装 UUID;在 fork、VM 快照、容器复制之后让生成器状态被回退;把 UUIDv3 / v5 误解为「每次都是新 ID」来使用;自行实现基于时刻的 UUID 或 UUIDv8 却对 clock rollback 与 counter 处理得很随意;把 UUID 在中途截断从而放弃了唯一性;以及没有在数据库中设置唯一约束,导致重复悄悄混入。
- 使用 UUIDv3 或 UUIDv5 会导致重复吗?
- UUIDv3 / v5 并不是不容易冲突的随机 ID,而是能够从相同名称重新生成相同 ID 的确定性 ID。RFC 9562 规定,由相同 namespace 与相同 canonical name 生成的 UUID 必须相等,因此相同输入得到相同 UUID 并不是事故,而是符合规范的正常行为。所以把它用于新建编号的场景就是误用。反过来说,如果 name 的规范化(canonicalization)出现偏差,同一个对象也会得到不同的 UUID。把 namespace 的设计与 name 的规范化规则明确写成规范,是很重要的一点。
- 要防止 UUID 重复该怎么做?
- 首先不要自行生成 UUID,而应尽量使用 uuid4() / uuid7() 这类标准 API,或者广泛使用的现成实现。把所用 UUID 的 version 作为规范明确下来,并确保在 fork、worker 重启、快照、克隆之后,不会继承旧的生成器状态。保存与比较时应保持完整的 128 位长度,不要把前缀比较或缩略显示当作本来的键来使用。在此基础上,如果真的不能容忍重复,就应在数据库中设置 UNIQUE / PRIMARY KEY 约束,并且不要吞掉 duplicate key 错误,而要能追溯到是哪个 generator 或哪个 node 产生的。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。