UUID v4 vs v7 vs ULID vs Snowflake:2026 分布式 ID 选型实战指南
分布式 ID 选型实战指南:UUID v4、v7、ULID、Snowflake ID、NanoID 在数据库性能、可排序性、存储大小和生态支持方面的全面对比,附多语言代码示例。
UUID v4 vs v7 vs ULID vs Snowflake:2026 分布式 ID 选型实战指南
选错 ID 方案代价高昂。在一张 1 亿行的表上,随机 UUID v4 主键比顺序 ID 多产生最高 10 倍的索引页分裂。Snowflake ID 需要中心化的 worker 注册机制,成为单点故障。ULID 曾是最佳折中方案——直到 UUID v7 以 IETF 标准的身份登场。
本文提供一套决策框架、性能实测数据和代码示例,帮你为系统选对标识符方案。
快速决策表
| 你的需求 | 最佳选择 | 原因 |
|---|---|---|
| 数据库主键(新项目) | UUID v7 | 时间有序,标准 uuid 列类型,索引性能最优 |
| 通用唯一 ID(无需排序) | UUID v4 | 全平台支持,零配置,122 位随机数 |
| 根据已知输入生成确定性 ID | UUID v5 | 相同的 namespace + name 始终生成相同 UUID |
| 高吞吐分布式系统(>10 万 ID/秒/节点) | Snowflake ID | 64 位整数,单 worker 内单调递增,原生 BIGINT 存储 |
| 短链接 / URL 安全 token / 客户端 ID | NanoID | 21 字符,URL 安全字母表,可自定义长度 |
| 已在使用 ULID 的遗留系统 | ULID | 保持现状——功能上与 UUID v7 等价,迁移不划算 |
UUID 版本详解
UUID v1 — 时间戳 + MAC 地址(已弃用)
UUID v1 编码 60 位时间戳和机器的 48 位 MAC 地址。它是最早的”可排序 UUID”,但有两个致命缺陷:泄露硬件身份,且使用非标准的时间纪元(1582 年 10 月 15 日)。RFC 9562 已正式弃用 v1,推荐使用 v6/v7。新项目请勿使用 v1。
UUID v4 — 纯随机
UUID v4 用密码学安全的随机数填充 128 位中的 122 位。它是使用最广泛的版本——简单、隐私、全平台支持。
优势:
- 零配置,无需协调
- 完全匿名——不泄露时间戳或硬件信息
- 所有数据库、语言和框架原生支持
劣势:
- 随机分布导致 B-tree 索引碎片化。在百万行级写密集表上,v4 主键的插入性能比顺序 ID 降低 2–10 倍,原因是过多的页分裂。
// 生成 UUID v4 — 所有现代浏览器和 Node.js 内置
const id = crypto.randomUUID();
// → "550e8400-e29b-41d4-a716-446655440000"
UUID v5 — 确定性哈希
UUID v5 使用 SHA-1 对 namespace UUID 和 name 字符串进行哈希,生成确定性的 UUID。相同输入始终产生相同输出。
适用场景: 从 URL、DNS 名称或其他可重复输入生成稳定 ID。优先选择 v5 而非 v3(v3 使用较弱的 MD5)。
import uuid
# 相同输入 → 相同 UUID,每次都一样
id = uuid.uuid5(uuid.NAMESPACE_DNS, "example.com")
# → "cfbff0d1-9375-5685-968c-48ce8b15ae17"
UUID v7 — 时间有序随机(推荐)
UUID v7(RFC 9562,2024 年 5 月)在最高有效位编码 48 位 Unix 毫秒时间戳,随后是 74 位密码学随机数。
为什么 v7 是数据库主键的新默认选择:
- 顺序插入:新 UUID 始终大于旧 UUID(毫秒精度内),B-tree 插入始终追加到索引末尾
- 与 v4 相比,写密集负载下页分裂减少最高 90%
- 天然按时间排序,无需额外的
created_at列 - 标准
uuid列类型——从 v4 迁移无需改表结构 - 74 位随机数——对绝大多数应用足够(v4 有 122 位)
权衡: 创建时间被嵌入 ID 中。如果需要不透露创建时间的不透明 ID,请使用 v4。
// UUID v7 生成(Node.js 20+)
import { v7 as uuidv7 } from "uuid";
const id = uuidv7();
// → "01906b5e-4a3e-7234-8f56-b8c12d4e5678"
// 旧 ID 始终排在新 ID 之前
PostgreSQL 和 MySQL 性能对比:v4 vs v7
在 PostgreSQL 16 上对 5000 万行表(B-tree 主键)的基准测试:
| 指标 | UUID v4 | UUID v7 | 提升幅度 |
|---|---|---|---|
| 插入吞吐量(行/秒) | 12,400 | 28,600 | 快 2.3 倍 |
| 5000 万行后的索引大小 | 4.2 GB | 2.8 GB | 小 33% |
| 批量插入期间的页分裂 | 120 万次 | 8.4 万次 | 减少 93% |
| 插入后的顺序扫描 | 320 ms | 180 ms | 快 44% |
在 MySQL/InnoDB 中,影响更为显著,因为主键就是聚簇索引——随机 v4 UUID 导致持续的页面重组,而 v7 的行为类似于自增 ID。
替代 ID 方案
ULID — v7 出现前的最佳方案
ULID(Universally Unique Lexicographically Sortable Identifier)创建于 2016 年,旨在解决 UUID v4 的排序问题。它将 48 位毫秒时间戳和 80 位随机数编码为 26 字符的 Crockford Base32 字符串。
01AN4Z07BY 79KA1307SR9X4MV3
|----------| |----------------|
时间戳 随机数
48 bits 80 bits
ULID vs UUID v7 —— 要不要迁移?
| 方面 | ULID | UUID v7 |
|---|---|---|
| 可排序 | 是 | 是 |
| 字符串长度 | 26 字符 | 36 字符 |
| 存储大小 | 16 字节 | 16 字节 |
| 标准化 | 社区规范 | IETF RFC 9562 |
| 原生数据库类型 | 否(CHAR(26) 或 BYTEA) | 是(uuid) |
| 语言支持 | npm、PyPI、crates.io | 大部分标准库内置 |
结论: 新项目选 UUID v7——同样的可排序性,加上更好的生态支持和原生数据库类型。已在用 ULID 的系统不必急于迁移,两者功能等价。
Snowflake ID — 高吞吐分布式系统
Snowflake ID(Twitter 2010 年创建)将以下信息打包进 64 位整数:
0 | 41 位时间戳 | 10 位 worker ID | 12 位序列号
- 41 位时间戳:自定义纪元起的毫秒数(约 69 年范围)
- 10 位 worker ID:支持 1,024 个独立 worker
- 12 位序列号:每个 worker 每毫秒最多生成 4,096 个 ID
优势:
- 8 字节——UUID/ULID 的一半,存入
BIGINT列 - 单 worker 内单调递增——保证节点内有序
- 单 worker 理论吞吐量 409.6 万 ID/秒
- 作为纯整数,人类可读
劣势:
- 需要中心化协调——worker ID 必须统一分配和管理(通常通过 ZooKeeper、etcd 或配置服务)
- 时钟偏移敏感——系统时钟漂移可能导致 ID 冲突或回退
- 自定义纪元——每个实现选择自己的纪元,跨系统互操作困难
- 非标准——存在数十种不兼容的变体(Twitter、Discord、Instagram 等)
// Snowflake ID 生成(使用 sony/sonyflake)
package main
import (
"fmt"
"github.com/sony/sonyflake"
)
func main() {
sf := sonyflake.NewSonyflake(sonyflake.Settings{})
id, _ := sf.NextID()
fmt.Println(id) // → 175928847299543040
}
何时选择 Snowflake: 系统每节点每秒生成 >10 万个 ID,需要紧凑的 64 位整数,且已有 worker ID 分配基础设施(如 Kubernetes Pod 序号)。
NanoID — 紧凑的 URL 安全 ID
NanoID 使用字母表 A-Za-z0-9_- 生成短(默认 21 字符)且 URL 安全的标识符。使用 crypto.getRandomValues() 保证安全性。
import { nanoid } from "nanoid";
const id = nanoid(); // → "V1StGXR8_Z5jdHi6B-myT"
const short = nanoid(10); // → "IRFa-VaY2b"
适用场景: 短链接、前端组件 key、邀请码、文件名——任何字符串长度敏感且不需要数据库级排序或跨系统互操作的场景。
不适用于: 数据库主键(无原生 DB 类型、不可排序、无时间戳)。
CUID2 — 大规模抗碰撞
CUID2 生成可变长度的 ID,专为水平扩展设计。它融合了计数器、时间戳、指纹和随机数。
适用场景: 需要在多个独立生成器之间保证碰撞抗性且无需协调的系统。实际上 UUID v7 以更好的标准化覆盖了这一需求。
全方位对比表
| 特性 | UUID v4 | UUID v7 | ULID | Snowflake | NanoID |
|---|---|---|---|---|---|
| 长度 | 36 字符 | 36 字符 | 26 字符 | 15–20 位数字 | 21 字符(默认) |
| 存储 | 16 字节 | 16 字节 | 16 字节 | 8 字节 | ~21 字节 |
| 可排序 | 否 | 是(时间) | 是(时间) | 是(时间) | 否 |
| 时间戳 | 无 | 48 位毫秒 | 48 位毫秒 | 41 位毫秒 | 无 |
| 随机数位数 | 122 位 | 74 位 | 80 位 | 12 位序列 | ~126 位 |
| 标准化 | RFC 9562 | RFC 9562 | 社区规范 | 专有 | 社区规范 |
| 原生 DB 类型 | uuid | uuid | 否 | BIGINT | 否 |
| 需要协调 | 否 | 否 | 否 | Worker 注册 | 否 |
| URL 安全 | 否(含连字符) | 否(含连字符) | 是 | 是(整数) | 是 |
| 100 万 ID 碰撞概率 | ~10⁻²² | ~10⁻¹⁸ | ~10⁻²⁰ | 零(单调递增) | ~10⁻²¹ |
各语言代码示例
JavaScript / TypeScript
import { v4 as uuidv4, v7 as uuidv7 } from "uuid";
import { ulid } from "ulid";
import { nanoid } from "nanoid";
// UUID v4
console.log(uuidv4());
// → "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
// UUID v7
console.log(uuidv7());
// → "01906b5e-4a3e-7234-8f56-b8c12d4e5678"
// ULID
console.log(ulid());
// → "01ARZ3NDEKTSV4RRFFQ69G5FAV"
// NanoID
console.log(nanoid());
// → "V1StGXR8_Z5jdHi6B-myT"
Python
import uuid
from ulid import ULID
from nanoid import generate
# UUID v4
print(uuid.uuid4())
# → "a8098c1a-f86e-11da-bd1a-00112444be1e"
# UUID v7(Python 3.14+ 计划内置,目前使用 uuid7 包)
from uuid_extensions import uuid7
print(uuid7())
# → "01906b5e-4a3e-7234-8f56-b8c12d4e5678"
# ULID
print(ULID())
# → "01ARZ3NDEKTSV4RRFFQ69G5FAV"
# NanoID
print(generate(size=21))
# → "V1StGXR8_Z5jdHi6B-myT"
Go
package main
import (
"fmt"
"github.com/google/uuid" // UUID v4 & v7
"github.com/oklog/ulid/v2" // ULID
gonanoid "github.com/matoous/go-nanoid/v2" // NanoID
)
func main() {
// UUID v4
fmt.Println(uuid.New())
// → "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
// UUID v7
fmt.Println(uuid.Must(uuid.NewV7()))
// → "01906b5e-4a3e-7234-8f56-b8c12d4e5678"
// ULID
fmt.Println(ulid.Make())
// → "01ARZ3NDEKTSV4RRFFQ69G5FAV"
// NanoID
id, _ := gonanoid.New()
fmt.Println(id)
// → "V1StGXR8_Z5jdHi6B-myT"
}
从 UUID v4 迁移到 v7
如果你的系统已使用 UUID v4 主键,想获得 v7 的性能优势,好消息是:v4 和 v7 共享相同的 128 位格式,存储在同一个 uuid 列类型中。 无需修改表结构。
迁移策略
- 新记录用 v7,旧记录保留 v4。 两者在同一列中共存。查询和 JOIN 完全一样。
- 更新 ID 生成代码——在应用层将
uuidv4()替换为uuidv7()。 - 不要重写已有的 v4 ID。 这会破坏外键、外部引用和缓存的 URL。
- 监控索引性能。 随着 v4/v7 比例向 v7 倾斜,索引碎片化会逐步降低。
兼容性检查
-- v4 和 v7 在同一个 uuid 列中共存
SELECT id, version FROM (
SELECT id,
CASE get_byte(id::bytea, 6) >> 4
WHEN 4 THEN 'v4'
WHEN 7 THEN 'v7'
ELSE 'other'
END AS version
FROM your_table
) t
GROUP BY version;
常见问题
应该用 UUID v7 还是自增整数?
自增整数更简单、更小(4–8 字节 vs 16 字节),但需要中心化的序列——只有数据库才能生成。UUID v7 可以在任何地方生成(客户端、边缘、微服务),无需数据库往返。简单的单数据库应用用自增;分布式系统、多租户架构或需要客户端生成 ID 的场景用 UUID v7。
UUID v7 的 74 位随机数够用吗?
够用。74 位随机数在每毫秒内提供 2⁷⁴ ≈ 1.9 × 10²² 种可能值。即使每毫秒生成 100 万个 ID,碰撞概率也仅约 10⁻¹⁰,远低于任何实际关切。UUID v4 的 122 位随机数对大多数应用来说是过度设计。
能从 UUID v7 中提取时间戳吗?
可以。前 48 位编码的是 Unix 毫秒时间戳:
function extractTimestamp(uuidv7) {
const hex = uuidv7.replace(/-/g, "").slice(0, 12);
const ms = parseInt(hex, 16);
return new Date(ms);
}
extractTimestamp("01906b5e-4a3e-7234-8f56-b8c12d4e5678");
// → 2024-07-01T12:34:56.000Z
这是特性而非 bug——但如果需要不透明 ID,请用 v4。
PostgreSQL 18 原生支持 UUID v7 吗?
PostgreSQL 18(2025 年发布)新增了内置的 uuidv7() 函数,无需 pgcrypto 或 pg_uuidv7 等扩展。MySQL 目前没有原生 v7 生成能力,需要在应用层生成。
为什么不直接用 ULID?
ULID 先于 UUID v7 出现,解决的是同一个问题。如今 v7 已成为 IETF 标准(RFC 9562),具备关键优势:原生 uuid 数据库类型(16 字节,高效索引)、更广泛的语言/框架支持、正式标准化。已在用 ULID 的系统可以继续用,新项目建议选 UUID v7。
什么时候 Snowflake ID 更合适?
当你需要在极高吞吐量(每节点 >10 万 ID/秒)下生成紧凑的 64 位 ID,且已有 worker ID 分配基础设施时。Snowflake 的 8 字节 BIGINT 存储是 UUID 的一半,在数十亿行规模下这一点很重要。代价是运维复杂度:你需要管理 worker ID 分配并处理时钟偏移。
需要立即生成 UUID?试试我们的 UUID 生成器——支持 v1、v4、v5、v7 批量生成和解码,100% 浏览器端运行。