UUID 实现原理
结论先行:UUID 是 128 位(16 字节)的结构化二进制标识,靠"固定位段布局 + 版本化算法"在不依赖中心节点的情况下实现全局唯一。2026 年的工程选型共识是:数据库主键等新场景默认用 v7(时间有序、索引友好);但要注意 v7 只是"近似有序",不是递增——同一毫秒内顺序随机,跨机器也不保证全局有序。
涉及的版本与标准
- RFC 4122(2005):定义 v1~v5,目前所有标准库默认生成的仍是这套下的 v4
- RFC 9562(2024-05):新增 v6 / v7 / v8,v7 为时间有序随机型
- Go:
github.com/google/uuidv1.6+(New()为 v4,NewV7()为 v7) - Python:3.14 起标准库内置
uuid6()/uuid7()/uuid8() - PostgreSQL:18(2025-09 发布)起原生提供
uuidv7(),并把uuidv4()作为gen_random_uuid()的别名 - MariaDB:11.7 原生支持 v4/v7;MySQL 只有 v1,v7 必须在应用层生成
UUID 的本质:128 位结构化布局
一个 UUID 是 128 位二进制,对外显示为 32 个十六进制字符、按 8-4-4-4-12 分组:
550e8400-e29b-41d4-a716-446655440000
↑ ↑
版本位 变体位
其中第 3 组首字符(第 13 个 hex)是版本号,第 4 组首字符(第 17 个 hex)是变体号(RFC 4122 变体的高两位固定为 10)。版本位和变体位是解码 UUID 的钥匙——任何实现读到这两个字段就知道该 UUID 是怎么生成的。
几个关键细节:
- v1 时间戳的起点:60 位时间戳按 100ns 递增,从 1582-10-15(格里高利历启用日)起算,量程约 3600 多年
- 时钟序列(14bit):检测到系统时钟回拨或节点地址变化时自增,防止新生成的 ID 与历史 ID 撞车
- v4 的 122 位随机:总空间 2^122 ≈ 5.3×10^36,随机源必须是密码学安全的(如
/dev/urandom、crypto/rand),否则可被预测 - v7 的时间前缀:48 位毫秒时间戳(Unix epoch)放在最高位,天然可排序,后 74 位随机抗枚举
各版本生成机制速览
| 版本 | 信息源 | 确定性 | 顺序性 | 典型用途 |
|---|---|---|---|---|
| v1 | 时间戳 + 时钟序列 + MAC | 否 | 基本有序 | 传统/兼容老系统 |
| v3 | 命名空间 + 名字 → MD5 | 是 | 否 | 同一实体稳定映射 |
| v4 | 122 位密码学随机 | 否 | 否 | 通用默认(无脑选它) |
| v5 | 命名空间 + 名字 → SHA-1 | 是 | 否 | 同 v3,哈希更强 |
| v7 | 毫秒时间戳 + 随机 | 否 | 是 | 数据库主键、消息 ID |
记忆比喻:v1 是快递面单(发货时间 + 单号序列 + 发货地址,能看出从哪台机器何时发出);v4 是从 2^122 个球的池子里摸一个,摸重概率约等于零;v3/v5 是身份证指纹,同一个名字永远算出同一个号;v7 是在随机球上盖了个时间邮戳,让数据库按时间入库、索引不随机分裂。
v4 与 v7 的 Go 实现
生产环境直接 github.com/google/uuid,核心逻辑手写也就十来行:
// v4:随机型,对应 uuid.New()
func newV4() [16]byte {
var u [16]byte
_, _ = rand.Read(u[:]) // crypto/rand 填满 128 位随机(必须密码学安全的)
u[6] = (u[6] & 0x0f) | 0x40 // 字节6高4位 → 0100,版本=4
u[8] = (u[8] & 0x3f) | 0x80 // 字节8高2位 → 10,变体=RFC 4122
return u // 最后按 8-4-4-4-12 转十六进制输出
}
// v7:时间有序型(RFC 9562),对应 uuid.NewV7()
func newV7(now time.Time) [16]byte {
var u [16]byte
ms := uint64(now.UnixMilli()) // 48 位毫秒时间戳,占字节 0~5
u[0], u[1], u[2] = byte(ms>>40), byte(ms>>32), byte(ms>>24)
u[3], u[4], u[5] = byte(ms>>16), byte(ms>>8), byte(ms)
_, _ = rand.Read(u[6:]) // 字节 6~15 随机
u[6] = (u[6] & 0x0f) | 0x70 // 版本=7
u[8] = (u[8] & 0x3f) | 0x80 // 变体=10
return u
}
要让项目"默认 v7",包一层统一入口即可:
import "github.com/google/uuid" // v1.6+
// 项目内统一 ID 入口:默认 v7(时间有序)
func NewID() uuid.UUID { return uuid.NewV7() }
用 PostgreSQL 18 的话连应用层都不用生成,直接在库里设默认值:id uuid PRIMARY KEY DEFAULT uuidv7()。
2026 年现状:v7 是不是默认?
要分两个层面看:
- 标准库 / API 层面:默认仍是 v4。Go 的
uuid.New()、Python 的uuid4()、Java 的UUID.randomUUID()、Node 的crypto.randomUUID()都没改默认,因为改默认会破坏兼容性。 - 新项目选型层面:默认已经翻转。数据库与 ORM 支持已成熟,社区共识是"新项目主键、事件 ID 默认用 v7",v4 退到防枚举、不泄露时间的场景。
各生态支持现状:
| 生态 | 标准默认 | v7 支持 |
|---|---|---|
| Go · google/uuid | v4 New() |
内置 NewV7(),需显式调用 |
| Python · 标准库 3.14+ | v4 uuid4() |
内置 uuid7()(3.14 新增) |
| Java · JDK | v4 randomUUID() |
JDK 未内置,需第三方库 |
| Node.js · crypto | v4 randomUUID() |
Web Crypto 规范固定 v4,npm uuid 提供 v7() |
| Rust · uuid crate | v4 new_v4() |
new_v7(),启用 v7 feature |
| PostgreSQL 18 | v4 gen_random_uuid() |
原生 uuidv7(),可作列默认值 |
| MariaDB 11.7 | — | 原生 v4/v7 |
| MySQL | v1 UUID() |
不支持,v7 只能应用层生成 |
v7 是"近似有序",不是递增
v7 的 128 位里只有前 48 位是毫秒时间戳,后 74 位是随机数,所以它有三个层次的行为:
- 同一毫秒内:完全随机。时间戳相同,排序由随机位决定,生成顺序与排序顺序无关
- 同一机器跨毫秒:基本有序。时间戳递增;但若系统时钟回拨(NTP 校时),仍会出现"新 ID 小于旧 ID"
- 跨机器:不保证全局有序。各节点时钟存在漂移,A 机后生成的 ID 完全可能排在 B 机先生成的 ID 前面
注意实现差异:RFC 9562 只规定布局,不强制同毫秒单调。Python 3.14 的 uuid7() 在随机位里内嵌 42 位计数器保证毫秒内单调,PG18 的 uuidv7() 把亚毫秒时间戳分数放进 rand_a 位段——这两个实现是单调的;而多数库(如 google/uuid 的 NewV7())是纯随机后缀,同毫秒内无序。
各方案递增性对比:
| 方案 | 同一毫秒内 | 跨毫秒(同机) | 全局唯一 | 递增性 |
|---|---|---|---|---|
| bigint 自增 | 严格递增 | 递增 | 单库内 | 严格递增 |
| 雪花 Snowflake | 序列号递增 | 递增 | 依赖时钟 | 单调 |
| v7 + 计数器(Python 3.14 / PG18) | 计数器递增 | 递增 | 时钟漂移时不保证 | 单调 |
| v7 纯随机实现(多数库) | 随机 | 基本有序 | 是 | 近似有序 |
| ULID | 随机 | 基本有序 | 是 | 近似有序 |
| v4 | 随机 | 随机 | 是 | 完全随机 |
对数据库而言,v7 的价值不是"递增",而是索引局部性:新插入的键都落在"当前时间戳附近",B+ 树每次都往同一片叶子区域插入,页分裂集中、缓存命中率高——比 v4 随机插入好一个量级。代价是高并发写入时所有节点都往"当前毫秒附近"挤,可能出现右侧热点页竞争(hot page contention),这是超大吞吐场景下 v7 主键的已知问题。
碰撞概率(v4 / v7)
v4 空间 2^122 ≈ 5.3×10^36。按生日悖论,生成约 2^61.5 ≈ 3.3×10^18 个后碰撞概率才过半——相当于每秒生成 10 亿个也要连续跑约 103 年。v7 后 74 位随机(约 2^74 空间)对于绝大多数业务同样足够;唯一需要注意的是随机源必须是密码学安全的,用 math/rand 这类可预测的随机源会破坏唯一性前提。
为什么不用时间戳或自增
- 自增 ID:需要中心协调(分布式多节点没有共享发号器,跨库合并必冲突)
- 纯时间戳:同一毫秒内必然重复,且可枚举泄露业务量
- UUID 把"唯一性"从集中协调变成本地生成 + 极小碰撞概率,代价是 128 位比 int64 大、v4 无序(这正是 v7 存在的理由)
选型建议
| 场景 | 推荐 |
|---|---|
| 数据库主键、事件/消息 ID、日志关联 ID(要可排序、索引友好) | v7 |
| 对外分享令牌、防枚举 ID、不能暴露创建时间的场景 | v4(v7 前缀会泄露毫秒级创建时间) |
| 分页游标、断点续传、要求"排序 = 创建顺序" | bigint 自增或雪花(Go 侧 sonyflake),v7 不满足严格递增 |
一句话总结:v7 保证的是"后一毫秒生成的大概率排后面",不是"后生成的就更大"。要严格递增,回到自增/雪花;要全局唯一又想要顺序性,v7 是 2026 年的平衡点。
浙公网安备 33010602011771号