UUID 完全指南:v4 vs v7 及使用场景
什么是 UUID?
UUID(Universally Unique Identifier,通用唯一标识符)是一个 128 位的标识符,在分布式系统中无需中央协调即可保证全局唯一性。它最常见的表示形式是被连字符分隔的 32 个十六进制字符:
550e8400-e29b-41d4-a716-446655440000
UUID 最初由 Apollo 计算机公司的 Network Computing System 使用,后来被标准化为 ISO/IEC 9834-8,并在 RFC 4122 中详细描述。如今它几乎是所有现代系统的默认标识符方案——从数据库主键到消息 ID、从会话令牌到文件名,随处可见。
UUID 之所以流行,关键在于”无需协调即可唯一”这一特性。多个节点可以独立生成 UUID,合并时几乎不可能发生冲突——128 位空间提供了约 3.4 × 10³⁸ 种可能值。
UUID 的版本
RFC 4122 定义了多个 UUID 版本,每个版本采用不同的生成策略。最常用的两个版本是 v4 和 v7,此外还有 v1(基于时间戳和 MAC 地址)、v3 和 v5(基于命名空间和哈希)。
UUID v4:随机性优先
v4 是目前最广泛使用的版本。它使用密码学安全的随机数生成器(或尽可能接近的来源)填充 122 个随机位,其余 6 位用于版本和变体标识。
// 使用原生 crypto API 生成 v4 UUID
const uuid = crypto.randomUUID();
console.log(uuid); // "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed"
// 手动实现(兼容性场景)
function uuidv4() {
const bytes = crypto.getRandomValues(new Uint8Array(16));
bytes[6] = (bytes[6] & 0x0f) | 0x40; // 版本 4
bytes[8] = (bytes[8] & 0x3f) | 0x80; // 变体 10
const hex = [...bytes].map(b => b.toString(16).padStart(2, '0'));
return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`;
}
v4 的优点是简单、快速、无状态,缺点是完全无序——这对数据库索引是个噩梦。
UUID v7:时间戳排序
v7 是 2024 年正式标准化的新版本(RFC 9562),专为现代数据库和分布式系统设计。它的前 48 位是 Unix 毫秒时间戳,剩余位是随机数。
// 生成 v7 UUID(简化实现)
function uuidv7() {
const timestamp = Date.now();
const bytes = new Uint8Array(16);
const view = new DataView(bytes.buffer);
// 前 48 位:时间戳
view.setUint32(0, Math.floor(timestamp / 0x100000000));
view.setUint16(4, timestamp & 0xffff);
// 剩余位:随机
crypto.getRandomValues(bytes.subarray(6));
bytes[6] = (bytes[6] & 0x0f) | 0x70; // 版本 7
bytes[8] = (bytes[8] & 0x3f) | 0x80; // 变体 10
const hex = [...bytes].map(b => b.toString(16).padStart(2, '0')).join('');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
由于时间戳位于高位,v7 UUID 按生成时间自然排序。这对数据库索引是巨大的优化。
v4 vs v7:如何选择?
| 特性 | UUID v4 | UUID v7 |
|---|---|---|
| 唯一性来源 | 纯随机 | 时间戳 + 随机 |
| 排序性 | 无序 | 按时间排序 |
| 数据库索引 | 性能较差 | 性能优秀 |
| 可追溯性 | 无法推断生成时间 | 可从 UUID 解析时间戳 |
| 实现复杂度 | 简单 | 略复杂 |
| 标准化 | RFC 4122(成熟) | RFC 9562(较新) |
数据库索引:为什么 v7 更好?
B-tree 索引在插入有序数据时性能最佳。v4 UUID 完全随机,每次插入都可能导致页分裂(page split)和索引重组,在写入密集型场景下性能会显著下降。
v7 UUID 按时间递增,新记录总是追加到索引末尾,几乎不会触发页分裂。对于 PostgreSQL、MySQL 等使用 B-tree 的数据库,这意味着:
- 写入吞吐量更高
- 索引碎片更少
- 缓存命中率更好
-- PostgreSQL 中从 v7 UUID 提取时间戳
SELECT
uuid,
to_timestamp(('x0000' || substr(uuid::text, 1, 8) || substr(uuid::text, 10, 4))::bit(64)::bigint / 1000.0) AS created_at
FROM users;
使用场景建议
选择 v4 的场景
- 临时 ID:会话令牌、一次性令牌、缓存键——不需要排序
- 匿名标识:不想暴露生成时间信息的场景
- 遗留系统:现有系统已使用 v4,迁移成本高于收益
- 简单优先:快速原型、脚本工具
选择 v7 的场景
- 数据库主键:尤其是高写入负载的表
- 事件溯源:需要按时间排序的事件流
- 日志和审计:ID 本身就携带时间信息
- 分布式系统:多节点写入同一数据库
- 消息队列:消息 ID 需要可排序
常见误区
”UUID 太长了,影响性能”
128 位确实比 64 位整数大,但现代数据库对此优化良好。对于大多数应用,UUID 带来的便利远超存储开销。如果存储空间真的紧张,可以考虑 ULID 或 NanoID 等更短的方案。
“v4 UUID 会冲突”
理论上,生成 2.71 × 10¹⁸ 个 v4 UUID 后才有 50% 的概率发生一次冲突。对于任何实际应用,这等同于”不会冲突”。
“UUID 不安全”
UUID 不是为安全设计的——v1 会暴露 MAC 地址,v7 会暴露生成时间。如果需要不可预测的令牌,请使用专门的密码学随机数生成器,而不是 UUID。
总结
UUID 是分布式系统中标识符的事实标准。对于新项目,优先考虑 v7——它解决了 v4 在数据库索引上的痛点,同时保持了 UUID 的所有优点。对于遗留系统或临时用途,v4 仍然是简单可靠的选择。
想快速生成 UUID?试试 CodeKit 上的 UUID 生成器——支持 v4 和 v7,所有生成都在浏览器中完成,数据不会发送到任何服务器。