← 返回博客首页

UUID 选型指南:v1 vs v4 vs v7 与 ULID 对比

为什么 ID 选型很重要

在分布式系统中,为每条数据分配唯一标识符是基础需求。选错 ID 方案可能导致数据库索引碎片化、排序困难、甚至隐私泄露。本文系统对比 UUID 各版本及 ULID,帮你根据场景做出正确选择。

UUID 基础

UUID(Universally Unique Identifier)是 128 位的全局唯一标识符,标准格式为 8-4-4-4-12 的十六进制字符串:

550e8400-e29b-41d4-a716-446655440000

UUID 的核心价值在于无需中央协调即可生成唯一 ID,这使其成为分布式系统的基石。UUID 规范历经演进,最新版本为 RFC 9562(2024 年 5 月发布,取代了 RFC 4122)。

各版本详解

UUID v1:时间戳 + MAC 地址

生成方式:当前时间戳(60 bit,精度到 100 纳秒)+ 机器 MAC 地址(48 bit)+ 时钟序列(14 bit)

优点

  • 可按生成时间排序
  • 无需随机数生成器

缺点

  • 隐私问题:MAC 地址暴露了机器物理位置
  • 安全风险:可推断生成时间和硬件信息
  • 多线程问题:同一机器多线程生成需协调时钟序列
  • 现代虚拟化环境中 MAC 地址可能重复

适用场景:仅限遗留系统兼容,新项目不推荐。

UUID v4:纯随机

生成方式:122 bit 随机数(6 bit 用于版本和变体标识)

优点

  • 实现简单,无需时间戳或机器标识
  • 无隐私泄露风险
  • 碰撞概率极低(2¹²² 空间)

缺点

  • 不可排序:完全随机,无法按生成顺序排列
  • 数据库索引灾难:随机插入导致 B+ 树频繁页分裂,索引碎片化严重
  • 存储为字符串时占用 36 字节

碰撞概率直觉:生成 2.71 × 10¹⁸ 个 v4 UUID 后,碰撞概率才达到 50%。每秒生成 10 亿个,需要约 85 年。

适用场景:不需要排序的通用唯一标识,如临时令牌、消息 ID。

UUID v6:重排的 v1

生成方式:与 v1 相同的时间戳 + MAC 地址,但时间戳高位在前,使得字典序与时间序一致。

定位:解决 v1 不可排序的问题,但保留了 MAC 地址的缺点。实际采用率低。

UUID v7:时间戳 + 随机(推荐)

生成方式:前 48 bit 为 Unix 毫秒时间戳,后 74 bit 为随机数(含版本/变体位)

优点

  • 按时间排序:字典序即时间序,完美适配数据库索引
  • 高性能写入:时间单调递增,B+ 树追加写入,无页分裂
  • 分布式友好:无需机器标识,各节点独立生成
  • 无隐私泄露:不含 MAC 地址
  • 兼容 UUID 格式:仍是标准 128 位 UUID

缺点

  • 时间精度为毫秒级,同一毫秒内依赖随机数保证唯一性
  • 可从 UUID 推断大致生成时间(通常不是问题,但需知晓)

适用场景:新项目的数据库主键、分布式事件 ID、可排序的唯一标识。2025 年的首选方案

UUID v8:自定义

RFC 9562 定义的 v8 允许完全自定义 128 bit 的布局。适用于有特殊需求的场景,如嵌入业务信息。不推荐普通场景使用。

ULID 方案

ULID(Universally Unique Lexicographically Sortable Identifier)是 UUID 之外的替代方案,专为可排序设计。

结构

01ARZ3NDEKTSV4RRFFQ69G5FAV
└── 时间戳(48 bit)──┘└── 随机(80 bit)──┘
  • 总长 128 bit,与 UUID 相同
  • 编码为 26 字符的 Crockford Base32 字符串
  • 时间戳为毫秒级 Unix 时间

ULID vs UUID v7

特性 UUID v7 ULID
长度 128 bit 128 bit
字符串表示 36 字符(含连字符) 26 字符
编码 十六进制 Crockford Base32
可排序 按时间字典序 按时间字典序
大小写 不敏感 不敏感
URL 友好 需处理连字符 无特殊字符
标准化 RFC 9562 社区规范
生态支持 原生 UUID 库 需专用库
时间精度 毫秒 毫秒

选择建议

  • 优先 UUID v7:如果你希望复用现有 UUID 基础设施和数据库原生类型
  • 优先 ULID:如果你更看重字符串短小、URL 友好,且愿意引入额外依赖

数据库主键选型深度分析

为什么随机 ID 索引性能差

以 MySQL InnoDB 为例,主键是聚簇索引,数据按主键顺序物理存储。UUID v4 随机插入意味着:

  1. 新行可能插入到 B+ 树的任意位置
  2. 频繁的页分裂(page split)导致写放大
  3. 索引碎片化,查询时需读取更多磁盘页
  4. 缓冲池命中率下降

而 UUID v7 / ULID 时间递增,新行始终追加到索引末尾,写入性能可提升数倍。

各方案对比

方案 排序 索引性能 存储成本 唯一性保障 适用规模
自增 ID 最优 4-8 字节 需中央分配 单机/小规模
UUID v4 16 字节 去中心化 中小规模
UUID v7 16 字节 去中心化 任意规模
ULID 16 字节 去中心化 任意规模
雪花算法 8 字节 需机器ID分配 大规模分布式

实战建议

  • 新项目:优先 UUID v7 或 ULID 作为主键
  • 已有系统:保持现有方案,迁移成本通常不值得
  • 超大规模:考虑雪花算法(Snowflake),8 字节更省空间
  • 公开 API:避免暴露自增 ID(可被枚举),用 UUID/ULID

安全注意事项

随机数质量

UUID v4 和 v7 的随机部分必须使用密码学安全的随机数生成器(CSPRNG)

  • Math.random():非加密安全,可预测
  • ❌ 简单的伪随机算法
  • ✅ Node.js:crypto.randomBytes()
  • ✅ Python:secrets 模块(非 random
  • ✅ Java:SecureRandom
  • ✅ Go:crypto/rand

不要用 UUID 做安全令牌

UUID 设计目标是唯一性而非不可预测性。会话令牌、CSRF Token、API Key 应使用专门的令牌生成方案(如 256 bit 随机 + HMAC)。

v1 的隐私风险

UUID v1 包含 MAC 地址,攻击者可推断:

  • 机器的物理网络位置
  • ID 的大致生成时间
  • 是否来自同一台机器

新系统绝对不要使用 v1。

各语言生成库推荐

语言 UUID v4 UUID v7 ULID
JavaScript/TS uuid uuid (v9.0+) ulid
Python uuid (stdlib) uuid-utils python-ulid
Java java.util.UUID com.github.f4b6a3:uuid-creator ulid-creator
Go google/uuid github.com/google/uuid (v1.6+) oklog/ulid
Rust uuid crate uuid crate (v1.10+) ulid crate

JavaScript 示例

import { v4, v7 } from 'uuid';
import { ulid } from 'ulid';

const idV4 = v4();  // 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
const idV7 = v7();  // '018f6b22-9c2a-7c8b-9e3f-4a5b6c7d8e9f'
const ul  = ulid();  // '01ARZ3NDEKTSV4RRFFQ69G5FAV'

Python 示例

import uuid
from ulid import ULID

id_v4 = uuid.uuid4()
id_v7 = uuid.uuid7()  # Python 3.14+
ul = str(ULID())

总结决策树

需要全局唯一 ID?
├── 需要按时间排序?
│   ├── 想用标准 UUID 格式? → UUID v7 ✅
│   └── 想要更短的字符串? → ULID ✅
├── 不需要排序?
│   └── UUID v4(通用场景)
└── 需要极高性能和短 ID?
    └── 雪花算法(需机器 ID 分配)
← 返回博客首页