跳到主内容
d.devtul.fun
EN
标识符 · 2026-09-20

什么是 UUID?写给开发者的完整指南

你肯定到处都见过它:550e8400-e29b-41d4-a716-446655440000。UUID 是数据库、消息队列和分布式系统里的默认标识符。但"直接用 UUID 吧"这句话背后藏着真正的选择——它有好几个版本,你选哪个,直接决定了从隐私到数据库性能的一切。这篇文章把它一次讲透。

UUID 是什么

UUID(Universally Unique Identifier,通用唯一标识符)是一个 128 位的值。写成标准形式是 36 个字符:32 个十六进制数字,分成五段用连字符隔开。

xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
8       - 4  - 4  - 4  - 12   = 36 字符

其中有两位被用来编码版本(M)和变体(N),其余部分由对应版本的规则来填充。正是这 128 位的宽度,让"通用唯一"成了一个站得住脚的说法,而不是宣传话术。

各个版本

版本怎么生成特点
v1时间戳 + MAC 地址可按时间排序;会泄露机器 MAC
v3命名空间 MD5 哈希给定名字结果确定
v4纯随机(122 位)无序;最常见的默认
v5命名空间 SHA-1 哈希确定型,比 v3 更强
v7Unix 毫秒时间 + 随机按时间有序,对数据库友好

v1 把时间戳和主机的 MAC 地址拼在一起。这让碰撞几乎不可能、而且值可以按时间排序,但它同时也把硬件标识广播了出去——在某些场景下这是个隐私问题。v3 和 v5 是命名空间的哈希:喂同一个名字和命名空间,每次都得到同一个 UUID,做幂等查询时很好用。v4 是纯随机,也是大多数库默认吐出来的版本。v7 把毫秒时间戳放在最前面,值按时间有序,尾部仍然保留随机性。

为什么分布式系统爱用 UUID

只有单一数据库时,自增整数主键简单又紧凑。一旦写入方超过一个,它就撑不住了:

  • 协调开销。两个节点同时插入,会去抢"下一个整数",除非你加一个中心化的序号发生器——而那既是单点故障,也是瓶颈。
  • 合并冲突。如果你要把两个数据库同步,它们各自的 id = 42 行会撞车,而且你分不清谁是谁。
  • 信息泄露。连续 id 会暴露你有多少用户,还让攻击者能顺着 /users/43、/users/44 一路枚举记录。

而任何一个节点上生成的 UUID,无需协调就是唯一的,合并时干干净净,也完全不透露你的体量。这就是为什么分布式日志、事件存储、多区域数据库都伸手去拿它。

碰撞概率,直观地看

v4 用了 122 个随机位,所以可能有 2^122 个值——大约 5 × 10^36。按生日悖论算,你得生成大约 2.6 × 10^18 个 v4 UUID,碰撞才变得可能。换个角度:就算你每秒生成十亿个 UUID,连生成一百年,离那个数字还差得极远。对任何普通应用来说,v4 碰撞根本不是真实风险。如果你在担心 v4 碰撞,那你比起 UUID 来有更大的架构问题要操心。

当数据库主键时的性能代价

这里有个会让团队吃惊的坑:随机 UUID 作为 B+ 树索引的主键,表现很糟。

自增键总是插在索引的"右边缘"——最新一行紧挨着上一行,所以插入很便宜,工作集也一直留在缓存里。而一个 v4 UUID 是随机的,每次插入都落在键空间的不同位置。B+ 树的页被打散,缓存局部性被摧毁,表一大就会看到索引碎片和更多的磁盘 I/O。

解法就是让标识符按时间有序。UUID v7(或者 ULID,一个类似的、用时间前缀的 128 位设计)把时间戳放在最前,于是插入都聚在最近的那一端,就像自增键一样——但又不牺牲去中心化。如果你必须用 UUID 当主键,优先选 v7 或 ULID,而不是 v4。

什么时候不该用 UUID

  • 在超大的、写密集的表上当主键却不做时间有序——你会为索引碎片付出代价。用 v7/ULID 或者复合键。
  • 在乎紧凑度的场合。一个 UUID 是 16 字节(文本形式 36 字节),整数才 4 或 8 字节。对于几十亿行、还有大量二级索引的表,这个体量会累积。
  • 需要人去手敲或肉眼读的时候。没人该手抄一个 UUID;任何面向用户的标识,用更短的 slug 或编码。

生成一个

多数语言都自带生成器。Node 里:

import { randomUUID } from 'crypto';
randomUUID();              // v4
// v7 通常要借助一个小库,例如 uuidv7()

想在浏览器里随手生成、或者批量生成,本站这个 UUID 生成器 在本地产出 v4 和 v7,不往服务器走任何一趟。

继续阅读