UUID 完全指南:v4 vs v7 及使用场景

CodeKit
uuid标识符数据库

什么是 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 v4UUID 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,所有生成都在浏览器中完成,数据不会发送到任何服务器。