UUID 生成器

为什么在数据库主键上 UUIDv7 胜过 UUIDv4

大约十年来,业界的常识是:"主键要用自增整数,因为随机 UUID 会摧毁数据库性能。"这个判断没错,但"UUID 很慢"这个结论被过度泛化了。真正的罪魁祸首是 UUIDv4 的完全随机性,而不是 UUID 这个类别本身。UUIDv7(于 2024 年 5 月在 RFC 9562 中标准化,取代旧的 RFC 4122)在高位放置 Unix 毫秒时间戳,低位放置随机位。相隔 1 毫秒生成的两个值按时间戳排序;同一毫秒内生成的两个值在该毫秒内随机排序。插入的形态由此彻底改变。 这一点之所以重要,原因在于 B-tree。PostgreSQL、MySQL/InnoDB、SQL Server —— 它们都以主键为键、按 B-tree(或 B+-tree)顺序存储行。插入一个主键大于所有现有键的值时,总是落在最右侧的叶子页。该页在缓冲池中处于热状态,下一次插入命中同一页,数据库几乎不需要任何 IO。而使用 UUIDv4 时,每次插入都落在随机的叶子上 —— 每次插入都要从磁盘拉入新页、把它弄脏、淘汰别的页,再把脏页写回。当表大到索引装不进内存时,UUIDv4 的插入可能比自增慢 10 到 50 倍,而且随机插入会在整棵树上到处引发页分裂,导致 B-tree 严重碎片化。 UUIDv7 消除了这个问题。插入按时间有序,因此落在索引的一小块热点区域 —— 并非总是字面意义上的最右叶子(随机低位会带来一些局部抖动),而是一个能一直留在缓存中的小工作集。PlanetScale、AWS 和 Percona 的基准测试均显示,在现代 InnoDB 和 Postgres 上,UUIDv7 的 INSERT 吞吐量与 bigint 自增的差距在 10% 到 20% 以内。困扰 UUIDv4 的碎片化也随之消失,因为新键在时间上总是彼此相邻。 有一个实践细节需要注意。InnoDB 原生以 16 字节 BINARY 存储 UUID —— 请存为 BINARY(16),而不是带连字符的 36 字符十六进制字符串。十六进制形式占 36 字节(去掉连字符是 32 字节),存储和索引占用都超过两倍,这直接意味着每页容纳的键更少、IO 更多。Postgres 有存储 16 字节的原生 uuid 类型;请使用它。在人工调试可读性与存储之间的标准权衡是:以二进制存储,仅在 API 边界渲染为十六进制。
// UUIDv4 layout: 122 random bits, 6 fixed bits
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
                ^^^^      ^^^^
                version 4 variant bits

// UUIDv7 layout: 48-bit Unix ms timestamp + 74 random bits
ttttttttttttt-tttt-7xxx-yxxx-xxxxxxxxxxxx
^^^^^^^^^^^^^ ^^^^      ^^^^
unix_ts_ms    rand-A    rand-B
              + version + variant

// Postgres
CREATE TABLE orders (
  id uuid PRIMARY KEY DEFAULT uuidv7(),  -- PG18+
  ...
);

// MySQL — store as BINARY(16), not VARCHAR(36)
CREATE TABLE orders (
  id BINARY(16) PRIMARY KEY,
  ...
);

// JS — generate UUIDv7 (no deps)
function uuidv7() {
  const ts = Date.now();
  const tsHex = ts.toString(16).padStart(12, '0');
  const rand = crypto.getRandomValues(new Uint8Array(10));
  rand[0] = (rand[0] & 0x0f) | 0x70; // version 7
  rand[2] = (rand[2] & 0x3f) | 0x80; // variant
  const r = [...rand].map(b => b.toString(16).padStart(2, '0')).join('');
  return `${tsHex.slice(0,8)}-${tsHex.slice(8)}-${r.slice(0,4)}-${r.slice(4,8)}-${r.slice(8)}`;
}

UUID 冲突概率 —— 用数字看生日悖论

"UUID 发生冲突的概率有多大?"是每个团队都会问一次的问题。答案的根基是生日悖论。UUIDv4 有 122 个随机位(另外 6 位固定用于 version 和 variant),所以名字空间有 2^122 ≈ 5.3 × 10^36 个不同的值。朴素的直觉会说:有这么多槽位,冲突不可能发生。但生日悖论告诉我们,冲突风险与 sqrt(N) 而非 N 成比例 —— 粗略的法则是,要达到首次冲突 50% 的概率,大约需要生成 sqrt(2^122) ≈ 2^61 ≈ 2.3 × 10^18 个值。 具体来说:每秒生成十亿个 UUIDv4,连续一百年,冲突概率仍然只在 10^-9 量级。总共生成一万亿个,其中出现任何冲突的概率大约是 10^-15 —— 比你今年被陨石砸中的概率还低约一百万倍。对单个应用而言,冲突风险实际上为零。只有当你把许多以极端规模生成的独立系统(想想运行数十年的全球 IoT 设备群)产生的 UUID 汇集在一起时,数学才开始变得有趣,而即便如此,务实的安全答案仍是:选定一种策略,永远不必担心。 现代 UUID 家族的版本表大致如下: v1 (1988): 时间戳 + MAC 地址。每台主机唯一,但会暴露主机的 MAC 和时间。基本已退役。 v3 / v5: namespace + name 的确定性哈希。相同输入总是产生相同的 UUID —— 适合派生 ID。 v4: 122 个随机位。2010 至 2024 年的主流选择。 v6: 类似 v1 但字节序可排序。小众。 v7 (2024 RFC 9562): 48 位 Unix 毫秒时间戳 + 74 个随机位。新系统的强烈首选。 v8: 自定义;各位由你自己填充。仅在 v7 无法满足需求时使用。 一条实用结论:不要手搓自己的 UUID 格式。正是那 6 个保留位(version + variant)让工具和数据库能够识别 UUID。一个 version 位设置错误的 v4 会被解释成其他版本,破坏比较、排序或驱动层面的优化。请始终使用经过验证的库,或上面的标准公式。
// Birthday paradox: probability of ANY collision among n values
// drawn from a space of size N is ≈ 1 - exp(-n^2 / (2N))

// For UUIDv4: N = 2^122
// n = 1 billion → P ≈ 4.7 × 10^-20
// n = 1 trillion (10^12) → P ≈ 4.7 × 10^-14
// 50% collision threshold: n ≈ 2.3 × 10^18

// Detect the version of a UUID
function uuidVersion(uuid) {
  // version is the first hex digit of the third group
  return parseInt(uuid[14], 16);
}
uuidVersion('018fb1c2-...-7abc-...');  // 7
uuidVersion('a1b2c3d4-...-4abc-...');  // 4

// Deterministic UUIDv5 (SHA-1 of namespace + name)
// Same name → same UUID, useful for migration mapping
import { v5 as uuidv5 } from 'uuid';
const NS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; // DNS namespace
uuidv5('example.com', NS); // always "cfbff0d1-9375-5685-968a-48ce8b50e3e0"

ULID vs UUIDv7 —— 选定一个,继续前进

在 UUIDv7 标准化之前,社区就已经发明了好几种按时间排序的 ID 格式。其中最流行的是 2016 年制定规范的 ULID(Universally Unique Lexicographically Sortable Identifier)。ULID 同样是 128 位 —— 48 位毫秒时间戳后接 80 位随机数 —— 但它用 Crockford Base32 编码,而不是带连字符的十六进制。其视觉格式是 26 个字符,形如 01HX1Y9ZQM7TWBKE6JR5VN8YGB。 在功能上,ULID 与 UUIDv7 非常相似。两者都按时间有序,都有毫秒时间戳前缀,都有充足的随机位,以文本存储时都能按字典序排序。差异在于审美和生态: 编码: ULID 是 Crockford Base32(26 字符)。UUIDv7 是带连字符的十六进制(36 字符)。ULID 字符串更短,并刻意避开易混淆的字符(没有 I、L、O、U)。就 URL 和人眼阅读而言,ULID 胜出。 数据库支持: UUIDv7 就是 UUID,因此所有具备 uuid 类型的数据库都能以 16 字节二进制高效存储它。ULID 没有原生类型 —— 你得存成 CHAR(26) 或 BINARY(16),并在边界处转换。 工具链: UUID 是通用语言。每种语言、每个 ORM、每个数据库都有数十年积累的 UUID 工具。ULID 有不错的库,但工具集成参差不齐。 规范成熟度: UUIDv7 是 RFC 9562(2024 年 5 月,IETF 标准)。ULID 是 GitHub 上的社区规范。 2026 年的默认选择是 UUIDv7,除非你有特定的 URL 美观理由偏爱 ULID。UUIDv7 拥有作为正式 RFC 的制度性势能,原生数据库支持真实存在且不断扩大(Postgres 18+、MySQL 9+、所有主流驱动),而且"直接用 UUID"与每位代码评审者和 DBA 已有的认知完全一致。 如果你需要面向最终用户展示的 URL 友好短 ID(想想 YouTube 的 /watch?v= 或 Stripe 的 cus_),UUIDv7 和 ULID 都不理想 —— 那类场景用的是 NanoID 风格的字母数字令牌:21 个字符、纯随机。不同的工作用不同的工具。在内部用可排序 ID 作主键,对外用 NanoID 作为面向客户的标识符。
// Side-by-side
UUIDv7: 018fb1c2-7abc-7d3e-8f4a-1b2c3d4e5f60   (36 chars, hex)
ULID:   01HX1Y9ZQM7TWBKE6JR5VN8YGB              (26 chars, base32)
NanoID: V1StGXR8_Z5jdHi6B-myT                    (21 chars, alphanumeric)

// Both UUIDv7 and ULID sort by time when stored as strings:
SELECT id FROM orders ORDER BY id; -- chronological without an extra index!

// UUIDv7 Postgres setup
CREATE EXTENSION IF NOT EXISTS pgcrypto; -- for older versions
CREATE TABLE orders (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- v4
  -- or in PG18+:  DEFAULT uuidv7()
  user_id uuid REFERENCES users(id),
  created_at timestamptz NOT NULL DEFAULT now()
);

// Generating ULID
import { ulid } from 'ulid';
ulid();                       // "01HX1Y9ZQM7TWBKE6JR5VN8YGB"
ulid(1714560000000);          // ULID for a specific timestamp
最后更新:

关于此工具

UUID 生成器输出标准 8-4-4-4-12 十六进制格式的 128 位标识符。v4 完全随机;v1 包含时间戳与 MAC;v7(较新)在前缀嵌入 Unix 毫秒时间戳,使 ID 可按时间排序 — 数据库喜欢这种特性,因为插入会集中在 B-tree 最右页。

使用方法

  1. 选择版本: 通用 ID 用 v4,数据库主键用 v7,需要 MAC + 时间则用 v1。
  2. 选择一次要生成多少个 UUID。
  3. 点击 Generate —— ID 会以便于复制的整洁列表显示在下方。
  4. 点击任意单个 ID 即可复制,或用 Copy All 复制整批。
  5. 把这些值放入你的迁移文件、fixture、环境变量或测试中。

常见用例

  • 为新数据库表生成主键,避免自增整数泄露业务体量。
  • 为分布式追踪日志生成关联 ID。
  • 用稳定且唯一的 ID 填充测试 fixture。
  • 为安全的 API 重试创建幂等性键。
  • 为对象存储中的文件命名,确保两个客户端永不冲突。
  • 把共享链接中的顺序 URL 替换为难以猜测的标识符。

常见问题

Q. 什么时候应该用 v7 而不是 v4?

A. 当你把 UUID 作为数据库主键存储时。v7 按时间排序,使 B-tree 插入保持在热点区域,避免了 v4 对行存储造成的随机写入代价。

Q. UUID v4 真的唯一吗?

A. 从数学上说,即使生成数十亿个,冲突的概率仍然基本为零 —— 前提是你的随机源是加密级的,而 crypto.getRandomValues 能保证这一点。

Q. 为什么 UUID v1 会暴露 MAC 地址?

A. 因为它设计于隐私尚未成为关注点的年代。多数现代 v1 实现会随机化 node 字段,但如果你在意隐私,请优先使用 v4 或 v7。

Q. UUID 区分大小写吗?

A. 不区分。十六进制数字 a-f 与 A-F 含义相同。大多数实现输出小写;两者比较时相等。