刚刚在一个知乎回答看到几百万uuid就出现冲突,这根本不可能!
于是,我干脆跑一亿uuid试试。
具体来说,我做了一个持续运行的实验:UUID 冲突实验室。
后端用 Java 的 UUID.randomUUID() 不停生成 UUIDv4,写进一张 MySQL 表,用主键做精确查重。
页面首屏就两个关键数字:表里现在有多少行,至今撞过几次。
写这篇文章时,行数已经超过 1 亿,冲突次数是 0。这两个数字还在变,以页面实时显示为准。
实验怎么搭的
表结构只有一列:
CREATE TABLE uuid_collision_seen (
id BINARY(16) NOT NULL PRIMARY KEY
) ENGINE=InnoDB;
UUID 去掉连字符后是 16 字节,直接存二进制。写入用多行 INSERT IGNORE,每批 5000 条,4 批并发;撞上主键的行会被自动跳过,写入不会中断。
只数 INSERT IGNORE 跳过的差值不够严谨,所以每批插入前会先查一次本批里哪些 UUID 已经存在,把冲突事件写进独立的账本表,记录 UUID、首次时间、最近时间和累计次数。页面上的「冲突判定」读的就是账本。主表超过 1.01 亿会被裁剪回 1 亿,账本是独立表,历史冲突计数不受影响。
后台任务每 10 秒检查一次行数,不够 1 亿就继续补齐。这个实验就能一直在跑。
页面上还能亲手做两件事:
- 粘贴任意一个 UUIDv4,走主键索引查它在不在这张一亿行的表里,返回是否命中和查询耗时;
- 点按钮现场追加 1,000 / 10,000 / 100,000 条,看真实耗时、新插入数、重复数和写入速度。
实测结果
截至写稿:
- 数据库实际行数:超过 100,000,000,原始 UUID 负载约 1.49 GiB
- 账本记录的冲突次数:0
1 亿条 UUIDv4 放进去,零冲突。
为什么 1 亿条撞不上
UUID 一共 128 位。UUIDv4 固定了 4 位版本号和 2 位变体位,剩下 122 位由随机数填充,取值空间:
2^122 ≈ 5.316 × 10^36
冲突概率由生日问题决定,关键在样本两两配对的数量。1 亿条 UUID 能组成的配对数:
100000000 × 99999999 / 2 ≈ 5 × 10^15(约五千万亿对)
至少出现一次冲突的概率近似:
P ≈ 1 - e^(-5×10^15 / 5.316×10^36) ≈ 9.41 × 10^-22
想把冲突概率推到 50%,样本量要到约 2.71 × 10^18 条。换算成时间:每秒生成 10 亿条、昼夜不停跑大约 86 年,才有一次五成机会撞出一对。
回头看「几百万条就撞了」的说法。几百万条对应的冲突概率在 10^-24 量级,比上面的 10^-22 还要低两个数量级。这个量级的事件真实发生,概率解释已经不够用,更合理的排查方向是实现链路,这里就不展开说了。
所以,UUID 是全球唯一吗
严格回答这个问题:UUIDv4 的唯一性是概率性的,标准本身没有承诺绝对唯一。
RFC 9562 没有设立任何中心登记机构,任意两台机器各自生成,数学上存在产生相同值的通道。它的唯一性建立在两个条件上:随机空间足够大(122 位),实现遵守规范且随机源足够好。
两个条件都满足时,1 亿条、10 亿条规模下的冲突概率低到可以忽略,工程上把它当全球唯一标识来用,这个用法有数学背书。任何一个条件被破坏,比如随机源退化或者 ID 被截短,唯一性承诺随之失效,百万级撞车就不奇怪了。
正确实现的 UUIDv4 可以在工程上视为全球唯一。它提供的数学保证是概率性的,而这个概率到底有多低,我已经用一亿条真实数据放进 demo 里了。
数据就挂在那里,可以自己查
实验还在跑。UUID 冲突实验室
首屏实时显示 MySQL 里的行数和冲突判定,你可以:
- 看当前这一亿行的实测状态;
- 把自己代码里生成的 UUIDv4 贴进去,查它有没有撞进这张表;
- 亲手点一次「追加 100,000 条」,给这张表再添一份样本。
下次再看到「几百万条就撞 UUID」的说法,可以先对照这里的实测数字,再去检查对方的生成链路。
参考来源
- RFC 9562《Universally Unique IDentifiers (UUIDs)》:UUIDv4 的版本位、变体位与 122 个随机位定义,https://datatracker.ietf.org/doc/html/rfc9562
- 生日问题近似公式:P ≈ 1 − e^(−n(n−1)/(2N))
- 本文实测数据来自 UUID 冲突实验室,页面实时刷新