← 返回博客
语言: English 中文
Dev 2026-05-27 9 分钟

UUID v4 与 v7:随机性、排序、隐私和数据库局部性

UUID 由 OSF 在 1980 年代末定义,2005 年作为 RFC 4122 正式化,2024 年 5 月以 RFC 9562 修订。9562 版加进了 v6、v7、v8 三个新版本,部分原因是当年的主流选择——随机 UUID v4——被发现并不适合数据库。新的 v7 才是 2026 年大多数团队应该默认伸手去拿的东西,但目前还没有,文档滞后大概两年。

UUIDRFC 9562RFC 4122UUIDv4UUIDv7ULID

UUID 是 128 bit 的数字,用 32 个十六进制字符按 5 段写成 xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx。这个数字可以由若干种算法生成,使用了哪种算法编码在数字本身里——4 bit 的 M(version)和 N 高位的 variant。这就是格式。有意思的部分是哪个算法生成了这些 bit,以及由此带来的后果。

各个版本

RFC 4122 定义了 v1 到 v5。RFC 9562 加进了 v6、v7、v8 和一个 "max" UUID(ffffffff-...-ffff)。八个版本里,真实系统中重要的就三个:

  • v1 —— 时间戳 + 时钟序列 + MAC 地址。按时间有序,但泄露生成主机的 MAC。
  • v4 —— 122 bit 随机。过去十年大多数语言和生态的默认。
  • v7 —— 48 bit Unix 毫秒时间戳 + 74 bit 随机。像 v1 一样按时间有序,不泄露 MAC,毫秒内仍然不可预测。

其余版本属于小众:

  • v2 —— DCE Security;几乎没人用。
  • v3 —— namespace + name,MD5 哈希。同输入确定性。常用于从 URL / DNS 名生成稳定 ID。
  • v5 —— 同 v3 但用 SHA-1。要用具名 UUID 就用 v5,不要用 v3。
  • v6 —— v1 的时间戳重排到能正确排序的版本。v7 已经覆盖了它的大多数用例。
  • v8 —— 自定义;除版本字段外其他 bit 由你支配。

如果你不在维护一个 2024 年前的老系统,你的选项基本上就是 v4 vs v7,再加上"我需要同输入产同 UUID"的特殊场景里的 v5。

为什么 v4 成了默认(以及为什么不该是)

支持 v4 的论点直白:随机、不需要时钟、两个生成器不需要协调、碰撞概率天文级低。RFC 4122 §4.4 给了数学:在 122 bit 随机值人群里,要生成大约 2^61(≈ 2 × 10^18)个 UUID,单次碰撞概率才超过 50%。实践上 v4 不会撞

反对 v4 的论点,只在系统规模上来后才显现:性能。具体来说:随机 UUID 当成 B-tree 索引数据库表的主键,会带来严重的写放大。

B-tree 索引按键排序。插入一行时,新键去到它该排到的叶页。键是顺序的(自增整数、按时间有序的 UUID),每次插入都进同一个最右叶页,热在内存里。键是随机的(v4),每次插入去到一个不可预测的叶页——几乎每次插入都碰一个冷页、把别的页驱逐、写回脏页。聚簇索引(MySQL InnoDB、SQL Server)下行本身的存储顺序也是这个问题。

不同负载基准结果不同,但写密集应用一致:从随机 UUID 切到按时间有序的 ID 做主键,吞吐通常提升 2 到 10 倍,有时更多。PostgreSQL 受影响较小(默认主键非聚簇),但 UUID 列上的索引照样有这问题。

为什么 v7 是现代默认

UUIDv7(RFC 9562):

0190b67c-1234-7abc-89de-123456789abc
^^^^^^^^^^^^^^                ^^^^^^^^^^^
48 bit Unix 毫秒时间戳        随机 bit

前 48 bit 是 Unix 毫秒时间戳,剩余 bit 是随机(其中几个用于 version / variant)。两个后果:

  • 可排序。 时刻 T 生成的 v7 UUID 排在 T+1ms 生成的之前。B-tree 插入命中热页。
  • 可解出时间。 你可以从任意 v7 UUID 里把毫秒时间戳取出来,不需要单独存。调试时方便("这一行是什么时候建的?")。

相比 v4 的代价:

  • 微小信息泄露。 拿到 v7 UUID 的人能读到生成时间。内部 ID 通常无所谓;公开场景偶尔不行(比如那种"生成时刻不能泄露"的公开 token)。
  • 随机性略低。 碰撞空间从 122 bit 缩到 74 bit,但仍极宽——按 1000 IDs/ms 这种极端速率,期望首碰撞要 2^37 个 ID,仍然是几十年量级。

数据库主键、不暴露敏感时间的公开 ID、事件标识——v7 是对的选择

需要"不可预测才是全部意义"的场景(nonce、session token),继续用 v4(或一个专门的密码学随机 token 格式)。

ULID:差点赢的那个格式

ULID 由 Alizain Feerasta 在 2016 年发布,比 UUIDv7 早八年。它解决和 v7 一样的问题,bit 布局略不同,编码也不同:

01ARZ3NDEKTSV4RRFFQ69G5FAV
^^^^^^^^^^   ^^^^^^^^^^^^^^^^
Crockford-base32 时间戳(48 bit ms)
            随机(80 bit)

26 个 Crockford Base32 字符(避开了 I L O U 这种容易混淆的字母),合 128 bit,可排序,按时间有序。和 v7 思路相同,bit 布局不同,编码不同。

ULID 在分布式系统和事件溯源圈赢得了思想份额;UUIDv7 赢了标准化。两者都行,选一个、坚持。UUIDv7 的优势:它就是 UUID(任何接受 UUID 的地方都能放,包括数据库 uuid 列)。ULID 的优势:文本表示更短(26 vs 36 字符)。

如果系统已经在存 UUID,要切到时间有序,用 v7。如果从零开始且看重短字符串,ULID 也合理。别纠结。

v1 的 MAC 脚注

UUIDv1 把生成主机的 48 bit MAC 放在最后一组里。最初的"唯一性机制"就是这个:每台机器 MAC 唯一,每台机器产出的 UUID 也唯一。同时它也让每个 ID 都泄露生成主机的 MAC

这是真实的隐私问题:2008 年微软 Word 文档通过嵌入的 UUID 泄露了作者 MAC 地址,至少一起刑事调查靠这个回溯到了恶意文档作者。现代 v1 实现有时会按会话随机化 MAC 字段(RFC 4122 §4.5 明确允许),但你从 UUID 本身看不出主机部分是真的还是随机的。

2026 年别再生成 v1 UUID 了。 v6 是规范给的迁移路径;如果你能选,v7 是更好的答案。

存储:不要把 UUID 当字符串存

UUID 是 128 bit = 16 字节。文本表示 36 字符 = 36 字节(去掉破折号 32 字节)。把 UUID 用 VARCHAR 存而不是原生 UUID / BINARY(16) / RAW(16),存储成本翻倍到三倍,索引更大,范围扫描性能也丢。大表上这就是真金白银。

  • PostgreSQL:原生 uuid 类型,16 字节。用它
  • MySQLBINARY(16) 加上辅助函数(UUID_TO_BIN / BIN_TO_UUID,对 v1 用 flag 1 重排以便排序)。MySQL 8.0 引入这两个;之前只能 VARCHAR(36) 然后忍着。
  • SQL Server:原生 uniqueidentifier,16 字节。注意:它的排序顺序很奇怪(从右往左字节比较),所以 v7 的排序在 SQL Server 上不被保留,除非你用心编码。
  • SQLite:没原生类型;用 BLOB(16 字节)省空间,或 TEXT 求可读。

人体工学答案:原生二进制存,渲染成文本只在 API 层做。

正确生成

v4:用密码学随机 bit。JavaScript 里 crypto.randomUUID()(浏览器、Node 19+)或 crypto.getRandomValues。Python 用 uuid.uuid4()。Go 用 crypto/rand + google/uuid 包。别用 Math.random() 自己造,熵不够。

v7:同样的密码学随机 + 当前 Unix 毫秒时间戳 + version/variant 位运算。多数现代 UUID 库直接支持 v7;不支持的话,算法在 RFC 9562 §5.7。注意细节:同一毫秒内 v7 要么自增计数器要么重新随机,否则紧循环里能产出重复。各库实现不一,自己测一下紧循环下是否单调递增。

常见坑

  • 随机 UUID 当聚簇主键。 写密集场景换 v7 / ULID。
  • UUID 用 VARCHAR(36) 存。 用原生类型。
  • v4 用了非密码学随机。 Math.random() 不是密码学随机;你会拿到重复,而且它们对攻击者可预测
  • 以为 UUID 格式 = 唯一性。 同输入两个系统按设计会产同样的 v3/v5。v1 即使随机化 MAC 也比 v4 碰撞概率高。v7 同毫秒同机器实现不当也会重复。
  • 把 v1 当 v7。 v1 的时间戳是从 1582-10-15 起的 100ns;v7 是从 1970 起的毫秒。纪元不同、单位不同。
  • 公开 ID 里嵌敏感时间戳。 如果 v7 UUID 的生成时间真的敏感(通常不该),就显式公开它,别指望别人不会把它取出来。
  • fork 服务器里 RNG 没正确重新播种。 预 fork 的 RNG 状态被多 worker 共享会产出重复 UUID。Linux 的 /dev/urandomgetrandom(2) 处理得对;老式 srand 风格 RNG 不行。
  • UUID 用字符串比较。 规范里大小写不敏感,许多字符串比较大小写敏感。比较前归一化为小写无破折号

迁移:v4 到 v7

如果现有系统用 v4 主键、想拿 v7 的性能,迁移大致这样:

  1. 加新列 id_v7 UUID
  2. 回填:新行用 v7;旧行原样放,或者用行的 created_at 作为时间戳部分生成 v7(让历史数据也按时间排序)。
  3. 应用切到 id_v7 当主键。
  4. 没有引用之后再删旧 id 列。

更激进的做法是重发 ID 并更新外键,那是大得多的变更。多数团队接受过渡期内同时存在两列 ID。

选哪个

  • 数据库主键、事件 ID、任何 B-tree 索引列:v7(或 ULID)。
  • 不能泄露时间的公开 ID:v4。
  • session token、nonce、CSRF token:v4(或专门的密码学 token 格式)。
  • 从已知字符串派生的稳定 ID(URL、文件哈希、DNS 名):v5。
  • 外部 API 期望某种 UUID:按对方规定。
  • 内部相关性 ID:任何按时间有序格式,确保调试时能把时间取出来。

2010 年代默认"全用 uuid4"在那时是合理选择。2026 年默认应该是 uuid7,需要不可预测性时再回 uuid4。

主要参考资料

用于核对本文技术细节的标准与官方文档。

在浏览器里直接生成 UUID 与 ULID

本站 UUID 工具用 Web Crypto API 在浏览器内生成 v1、v4、v7 与 ULID,可解析 v1/v7/ULID 内嵌的时间戳,也可用于批量生成测试数据。数据不会离开浏览器。

打开 UUID 工具

相关文章

继续阅读同一主题领域的实践指南。

查看全部文章

Cookie 同意

我们使用 Cookie 来增强您的体验并展示相关广告。您可以自定义您的偏好。