为什么 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 随机插入意味着:
- 新行可能插入到 B+ 树的任意位置
- 频繁的页分裂(page split)导致写放大
- 索引碎片化,查询时需读取更多磁盘页
- 缓冲池命中率下降
而 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 分配)