122 位随机空间把碰撞概率压到远低于硬件故障的水平。
UUID 明明可能重复,为什么大家还敢拿它当唯一值?
软件开发里有一个看起来挺矛盾的做法:UUID 在数学上并不能保证永不重复,但大量系统照样拿它当数据库主键、请求标识、文件标识、消息编号甚至订单记录的唯一 ID。
既然重复的可能性确实存在,按墨菲定律的说法——只要一件事有可能出错,它迟早会出错——工程师凭什么还敢这么用?
这个问题背后藏着软件工程里一个很重要的思想:工程系统追求的从来不是绝对安全,而是在成本、复杂度和收益之间,把风险压到可以接受的程度。
从生日悖论算一算 UUID 的真实碰撞概率,看工程师如何用分层防御把一个理论风险变成可以放心忽略的事,以及哪些用法会把这份安全感亲手毁掉。
- UUID 的碰撞概率到底多低
- 墨菲定律的正确打开方式
- 数据库唯一约束是最后防线
- 截断 UUID 有多危险
- UUID 和自增 ID 怎么选
UUID 的可靠不是靠单点保证,而是多层机制叠出来的。
数据库主键或唯一索引确保万一冲突也不会写入脏数据。
插入失败就重新生成再提交,二次冲突的概率更是微乎其微。
幂等键和业务唯一约束防止同一件事被重复执行——这是 UUID 管不了的。
UUID 从来没有承诺绝对唯一
UUID 全称 Universally Unique Identifier,通用唯一标识符,是一组 128 位的数据,长这样:
550e8400-e29b-41d4-a716-446655440000名字听起来像是在承诺“这个值在整个宇宙中都是唯一的”,但严格来说,它表达的是一种工程意义上的唯一性:在正常使用方式和合理数据规模下,重复概率低到通常可以忽略。
最常见的 UUID v4 依赖随机数生成。总长度虽然是 128 位,但有几位要用来标记版本和格式,真正用于随机取值的大约是 122 位,也就是
2122≈5.3×1036
种可能的取值。
这个数字大到什么程度?一个系统就算每天生成一亿个 UUID、连续跑上很多年,用掉的也只是全部空间中极其微小的一角。
生成十亿个 UUID,重复概率有多大
看到 5.3×1036 这个数,很多人会直觉地认为:只要没把所有值用完,就不会重复。
这个直觉是错的。随机数不是按顺序依次领取的,同一个值理论上随时可能被再次抽中,所以 UUID 的碰撞概率要用生日悖论来估算。生成 n 个 UUID 时,发生至少一次碰撞的概率大致是:
P≈2×2122n(n−1)
代入几个数量级感受一下:
| 累计生成量 | 至少一次碰撞的概率 |
|---|---|
| 十亿(109) | 约 9.4×10−20 |
| 一万亿(1012) | 约 9.4×10−14 |
9.4×10−20 是什么概念?一个正确使用 UUID v4 的系统,更应该担心的是硬盘损坏、内存翻转、网络异常、数据库误操作、代码缺陷、配置错误、机房断电和人为失误——UUID 随机碰撞在这份清单里排不上号。
从纯数学角度看,重复仍然可能发生;但在现实工程环境里,它已经被压缩成一个远低于大量日常故障的风险。
墨菲定律不是让你消灭一切可能性
墨菲定律常被概括成“凡是可能出错的事情,最终都会出错”。这句话的真正价值,是提醒工程师不要轻视失败场景,不要因为概率低就假设某件事永远不会发生。
但它不是让你为每一种理论风险投入无限成本。服务器可能遭遇地震、洪水、火灾、雷击,甚至被坠落物砸中,企业却不会为每一台普通服务器建一座地下堡垒。
风险管理看三个因素:发生的概率、发生后的损失、降低风险要付出的成本。如果一个风险概率极低、影响有限,而且已经有成本很低的兜底措施,继续花大代价追求“绝对不发生”,反而是不合理的。
UUID 正是这种思路的典型例子:它无法从数学上保证永不重复,却用极低的使用成本,把碰撞概率压到远小于其他常见事故的程度。
数据库唯一约束才是最后防线
可靠的系统不会因为碰撞概率低,就直接相信应用程序生成的值一定不重复。数据库里照样要设主键或唯一索引:
CREATE TABLE records (
id UUID PRIMARY KEY,
content TEXT
);分工很清楚:UUID 负责让冲突几乎不会发生,唯一约束负责确保冲突真的发生时不会悄悄写入脏数据。
万一插入时真的撞上了,数据库会拒绝这次写入,应用程序重新生成一个 UUID 再提交就是了。第一次冲突本身已经极其罕见,重试后再撞一次的概率更是低到可以不计——这套机制在工程上已经足够可靠。
这就是软件系统里常见的分层防御:前一层降低问题发生的可能性,后一层阻止问题造成实际破坏。
真遇到 UUID 重复,先查程序
如果一个实际系统真的出现了 UUID 重复,第一反应不应该是“122 位随机空间被正常撞中了”,而应该是排查实现。常见的肇事者:
- 随机数生成器质量太差;
- 多个进程错误地使用了相同的随机种子;
- 虚拟机克隆后保留了相同的随机状态;
- 程序把固定字符串误当成随机 UUID;
- 为了让 ID 好看,截取了 UUID 的一小部分。
其中最危险、也最常见的就是最后一条——随意截断 UUID。
import uuid
short_id = uuid.uuid4().hex[:8]完整的 UUID v4 有约 122 位随机空间;只保留前 8 个十六进制字符,随机空间就只剩 32 位,也就是 232≈43 亿种可能。43 亿听着还是很大,但生日悖论在这里同样起作用:只需要生成七万多个值,碰撞概率就接近 50%。
长得像 UUID ≠ 拥有 UUID 的安全性
截断后的短 ID 用于页面展示、临时追踪这类不要求严格唯一的场景问题不大;一旦被用作大规模业务数据的唯一主键,风险会快速上升。要用 UUID,就保留完整值、使用操作系统提供的可靠随机源,并且照常设置数据库唯一约束。
为什么不用绝对不会重复的自增 ID
很多数据库都支持自增整数主键:
id BIGINT AUTO_INCREMENT PRIMARY KEY在单机数据库或集中式系统里,自增 ID 简单、高效、占用空间小,还天然有序,通常是非常好的选择。
但自增 ID 依赖一个统一的分配中心。如果北京、上海、广州三个节点都要生成全局唯一 ID,它们要么都访问同一个数据库,要么提前划分号段,要么额外建一套全局发号服务。系统一旦进入多机房、分库分表、离线运行、客户端本地创建数据或者多系统数据合并的场景,这种集中协调就会越来越麻烦。
UUID 的核心价值恰恰是任何节点都可以独立生成,无须提前询问其他人:北京的节点可以生成,上海的节点可以生成,客户端离线时也可以生成,不同系统的数据合并时通常不需要重新编号。
工程师接受一个极低的理论碰撞风险,换来的是系统结构上的巨大便利——少了网络依赖、中心节点压力和系统间的协调成本。
UUID 也不是万能的
UUID 方便,但缺点同样明显。普通整数主键只要 8 字节,UUID 要 16 字节,用字符串形式存还会更大;随机 UUID 缺乏顺序性,新记录可能插进数据库索引的任意位置,导致 B 树索引频繁分裂、缓存局部性变差,高写入场景下会拖累性能。
所以实际选型时有一整个谱系可挑:
| 方案 | 特点 | 适合场景 |
|---|---|---|
| 自增 ID | 简单高效、有序、依赖分配中心 | 集中式数据库 |
| 雪花算法 | 大致有序的分布式 ID,需管理机器编号和时钟 | 大规模分布式系统 |
| UUID v7 | 兼顾时间顺序和分布式生成 | 需要分布式生成又在意索引性能 |
| UUID v4 | 生成简单、节点零协调,索引性能一般 | 去中心化、离线生成 |
也有系统两头都要:数据库内部用整数主键,对外额外暴露一个 UUID 作为外部标识。
技术选型的关键从来不是找一个各方面都完美的方案,而是根据系统规模、部署方式、查询模式、可靠性要求和维护成本,选最适合当前问题的那个。
业务唯一性不能只靠 UUID
还有一个经常被忽视的问题:记录 ID 唯一,不等于业务操作唯一。
用户因为网络超时连点了两次“提交订单”,系统可能分别生成两个完全不同的 UUID,成功创建两条订单记录。UUID 没有冲突,主键完全正常,但业务层已经出现了重复订单。
所以关键系统还需要建立业务层面的唯一约束——订单请求号、支付流水号、运输指令编号,或者由客户端提供的幂等键:
CREATE UNIQUE INDEX unique_request_id
ON orders(request_id);UUID 解决的是“一条记录如何获得一个低冲突的技术标识”,业务唯一约束解决的是“同一件事是否被重复执行”。这两个问题看起来相似,实际位于完全不同的层面。
可接受风险才是工程世界的常态
UUID 被大量使用,不是因为工程师相信它绝对不会重复,而是因为在正确生成、完整保存、数据库唯一约束和失败重试都到位的情况下,UUID 碰撞引发实际事故的概率,已经低到没必要为它付出更多成本。
墨菲定律提醒我们要考虑“碰撞发生后怎么办”——所以数据库要有唯一约束,程序要能处理插入失败,关键业务还要有额外的幂等检查。概率论则告诉我们,完整 UUID 的随机碰撞极其罕见——所以系统没必要为了数学意义上的绝对唯一,去背一个复杂、昂贵而脆弱的集中发号体系。
软件工程的可靠性通常来自多层机制的叠加:巨大的随机空间降低碰撞概率,数据库约束阻止重复写入,异常处理完成重新生成,业务约束避免重复操作。这些机制组合在一起,UUID 虽然没有做到理论上的绝对唯一,却已经足以成为现实世界里非常可靠的唯一标识符。
- UUID v4 有约 122 位随机空间,生成十亿个的碰撞概率约 9.4×10⁻²⁰,远低于硬件故障等日常风险。
- 碰撞概率要用生日悖论估算,而不是看空间有没有用完;截断 UUID 会让安全性断崖式下跌。
- 分层防御:随机空间降低概率、数据库约束兜底、失败重试恢复、业务幂等键防重复操作。
- 工程追求的不是绝对安全,而是在成本、复杂度和收益之间把风险降到可接受的程度。
版权所有
版权归属:Shuo Liu
