UUID 完全指南:从 RFC 4122 到 RFC 9562,理解 v1~v8 的结构、生成算法与工程选型

在分布式系统中,生成 ID 看起来是一件很小的事,却涉及两个完全不同的问题:如何在不依赖中心节点的情况下避免标识冲突,以及这个 ID 在数据库和业务中是否好用。

MySQL 自增主键解决了单库内的编号问题,却很难直接满足多服务独立生成、离线创建、跨库合并等需求。UUID(Universally Unique Identifier,通用唯一标识符)提供了另一种思路:每个节点独立按照约定的算法生成 128 位标识符,不必先向统一的发号器申请号码。

不过,UUID 不等于随机字符串,UUID 的不同 Version 也并非单纯的软件版本升级。 有的 UUID 依赖时间和节点信息,有的依赖哈希,有的完全依赖随机数,还有的专门针对数据库的时间有序写入进行了设计。

本文结合 RFC 4122、RFC 9562 以及相关技术资料,把 UUID v1~v8 的结构、生成规则、适用场景和工程实践放在同一套框架中说明。

标准版本说明:2005 年发布的 RFC 4122 已被 2024 年发布的 RFC 9562 取代。本文以 RFC 9562 为当前规范,回溯 RFC 4122 对 v1~v5 的定义,并说明新增的 v6、v7、v8。UUIDv2 的具体生成格式源自 DCE Security 规范,RFC 9562 仅保留其编号,不重新定义其细节。

一、先给出结论:应该选哪个 UUID?

需求 优先考虑 原因与注意点
只需要分布式生成、不可预测性较好的普通标识符 UUIDv4 122 位随机空间,成熟且广泛支持;用于 B-Tree 主键时插入局部性通常较差
新系统使用 UUID 作为数据库主键,希望大致按时间排列 UUIDv7 48 位 Unix 毫秒时间戳位于高位,通常更利于索引写入
同一命名空间、同一名称必须得到相同 ID UUIDv5 基于 Namespace + Name 的 SHA-1 确定性生成;不等于加密或签名
与既有 UUIDv1 系统兼容,但希望改善排序和索引局部性 UUIDv6 重新排列 v1 的时间字段;纯新系统通常优先考虑 v7
维护历史上依赖 MAC/时间戳格式的系统 UUIDv1 兼容优先;新系统一般不主动选择
维护 DCE Security / POSIX 身份编码系统 UUIDv2 专用、少见、支持有限,不适合普通业务 ID
兼容既有 MD5 名称型标识 UUIDv3 新的名称型需求通常采用 v5
必须设计企业私有 UUID 位布局或 SHA-256 名称型标识 UUIDv8 需要自行约定字段含义、唯一性和跨系统互操作规则

简化成一句话:不需要排序用 v4;需要时间有序的 UUID 优先看 v7;需要确定性映射用 v5;其他版本通常因为历史兼容或特殊协议约束才使用。

这只是选型起点,不意味着 v7 总能取代自增 BIGINT 或雪花 ID。数据库规模、索引组织方式与 ID 的使用边界依然需要一起考虑。

二、UUID 的 128 位究竟怎么组织?

2.1 UUID 的基本表示

一个 UUID 占用 128 bit = 16 byte,通常显示为 32 个十六进制数字,按照 8-4-4-4-12 分为五组,加上四个连字符后总长为 36 个字符:

1
2
3
4
5
6
f81d4fae-7dec-11d0-a765-00a0c91e6bf6
|------| |--| |--| |--| |----------|
8 4 4 4 12 (十六进制字符数)

16 字节 = 128 位
32 个十六进制字符 × 4 位 = 128 位

其二进制值也可以存成连续的 16 字节,而不必保留字符串中的连字符。

UUID 是通用技术名称;GUID(Globally Unique Identifier)常见于 Microsoft 生态,两者在日常工程中通常指兼容的 128 位标识符。但是,Windows COM GUID 的部分二进制序列化方式存在小端字节序历史差异,不能不加区分地直接交换原始字节数组。

2.2 Version 与 Variant 不是一回事

UUID 的两个固定标识字段是:

  • Version(版本):整个 UUID 的 bit 48~51,即第 7 个字节(从 0 开始)的高 4 位,显示在第三组十六进制数字的第一个位置。它决定 UUID 的生成算法。
  • Variant(变体):RFC 9562 格式中的 bit 64~65,即第 9 个字节的高 2 位,固定为 10。它决定其余位应按哪类 UUID 布局解释。

例如:

1
2
3
4
5
6
7
8
9
xxxxxxxx-xxxx-7xxx-8xxx-xxxxxxxxxxxx
^ ^
| └── Variant:8、9、a、b 之一(高两位均为 10)
└─────── Version:7

xxxxxxxx-xxxx-4xxx-axxx-xxxxxxxxxxxx
^ ^
| └── Variant:a = 1010,属于 RFC 9562 变体
└─────── Version:4

不要把 Variant 误认为 Version。 同样是符合 RFC 9562 的 UUID,v1~v8 都使用 10 这一变体;差异体现在四位 Version 以及剩余字段的含义上。

Variant 的前导位 第四组首个十六进制字符范围 含义
0xxx 0~7 兼容历史 NCS 格式
10xx 8~b RFC 4122 / RFC 9562 格式,本文的主要对象
110x c~d 保留给 Microsoft 历史兼容格式
111x e~f 保留供将来定义

上面的 Variant 有不同长度的前缀,不能简单理解为始终存在一个独立的 4 位 Variant 字段。对于本文讨论的 RFC 格式,其固定高两位是 10。

2.3 早期六字段布局,不能直接套到所有版本

RFC 4122 的经典 UUIDv1 布局可以写成:

1
2
3
4
5
最高位                                                          最低位
┌──────────────────┬──────────┬──────┬──────────┬─────┬──────────────┬────────────────────┐
│ time_low │ time_mid │ ver │ time_hi │ var │ clock_seq │ node │
│ 32 bit │ 16 bit │ 4bit │ 12 bit │ 2bit│ 14 bit │ 48 bit │
└──────────────────┴──────────┴──────┴──────────┴─────┴──────────────┴────────────────────┘

历史名称 time_low、time_mid、time_hi_and_version 等来自 v1 的设计。

但对于 v3、v4、v5,这些位置根本不存放时间;对于 v6、v7、v8,相应字段的名字和语义也发生了变化。 因此阅读新版标准时,应根据各版本对应的位布局理解数据,而不是把 v1 的六字段解释强加给所有 UUID。

2.4 两种时间纪元

时间体系 使用版本 起点 时间单位 时间位数
Gregorian Epoch v1、v6;v2 继承部分字段 1582-10-15 00:00:00 UTC 100 纳秒 tick 60 位(v2 部分被覆盖)
Unix Epoch v7 1970-01-01 00:00:00 UTC 毫秒 48 位

v1/v6 的时间计数大致按下式构造:

1
uuid_ticks = unix_milliseconds × 10_000 + 122_192_928_000_000_000

其中常量表示两个纪元之间相差的 100 纳秒计数。格式具有 100 纳秒刻度,不代表操作系统时钟真的能达到 100 纳秒精度;生成器可以借助计数器处理同一时钟刻度中的多次生成。

三、UUID v1~v8:总览比较

版本 核心材料 主要数据字段 是否确定性 时间可排序性 典型使用场景 新项目建议
v1 Gregorian 时间 + 时钟序列 + 节点标识 时间 60 位、Clock Sequence 14 位、Node 48 位 否 时间字段顺序不理想 历史 RPC、兼容系统 一般不推荐
v2 v1 派生 + 本地身份 ID、Domain Local ID 32 位、Domain 8 位,保留部分时间与节点字段 否 不适合通用时间排序 DCE Security / POSIX 身份映射 专用场景
v3 Namespace + Name → MD5 MD5 摘要有效位共 122 位 是 否 兼容历史名称映射 优先替换为 v5(兼容除外)
v4 加密安全随机数 随机位 122 位 否 否 普通随机 ID、跨系统对象 ID 推荐
v5 Namespace + Name → SHA-1 SHA-1 摘要截取并覆盖控制位,122 位有效位 是 否 DNS、资源名称、可复现标识 推荐
v6 v1 时间字段高位前置 时间 60 位、Clock Sequence 14 位、Node 48 位 否 跨时间刻度基本有序 v1 演进、兼容型数据库系统 特定场景
v7 Unix 毫秒时间 + 随机值/可选计数器 时间 48 位、剩余 74 位 否 按毫秒时间有序 分布式数据库主键、事件 ID 推荐
v8 自定义算法 自定义 122 位 由实现决定 由实现决定 厂商私有协议、SHA-256 名称型 ID 充分约束后使用

表中的“有效位”均指扣除 4 位 Version 和 2 位 Variant 之后的 122 位。名称型 UUID 的有效位来自摘要,不代表拥有与 122 位均匀随机数完全相同的对抗碰撞性质。

四、UUIDv1:时间、时钟序列与节点 ID

4.1 位结构

1
2
3
4
5
6
7
8
UUIDv1(总计 128 bit)
[time_low:32][time_mid:16][ver=0001:4][time_high:12][var=10:2][clock_seq:14][node:48]

时间戳实际拼接顺序:
T(60bit) = [time_high:12][time_mid:16][time_low:32]

但 UUID 的存储/文本顺序:
[time_low:32][time_mid:16][time_high:12] ← 低位在前
字段 位数 作用
time_low 32 60 位时间戳的最低 32 位
time_mid 16 时间戳中间 16 位
time_high 12 时间戳最高 12 位
version 4 固定 0001
variant 2 固定 10
clock_seq 14 处理时钟回拨、节点状态变化等冲突风险
node 48 通常来自 MAC,也允许使用随机节点标识

4.2 生成规则

  1. 获取当前时间,将其转换为从 1582-10-15 开始的 100 纳秒单位计数。
  2. 根据规范维护时钟序列及必要状态,降低时间回拨、节点 ID 变化、重复时间刻度带来的冲突风险。
  3. 选择 48 位节点标识,可以使用 MAC,也可以依据规范使用随机节点值。
  4. 把时间戳分别拆成 32 + 16 + 12 位,连同时钟序列、节点标识打包。
  5. 将 Version 固定为 1、Variant 固定为 10。

v1 的唯一性依赖时间、节点标识、时钟序列与实现状态管理的共同约束,不是只要把当前时间和 MAC 拼接起来就绝对不会重复。

4.3 问题与推荐场景

v1 把不断变化最快的时间低 32 位放在最前端,因此按照标准字节序进行比较时,不能在整个时间范围内维持自然的递增顺序。这也是 v6 要调整字段顺序的直接动机。

另外,部分实现会嵌入真实 MAC,泄露生成环境相关信息;通过随机节点标识可以减少这一问题。

适合:读取和兼容历史 v1 数据、必须与旧 RPC 或 UUID 生成器保持一致的系统。

不建议:新设计的通用数据库主键、期望隐藏生成环境的公开标识。

五、UUIDv2:DCE Security 身份型 UUID

UUIDv2 是最容易被误解的版本。RFC 4122 记录其用途是 DCE Security,RFC 9562 继续保留编号,但明确指出 v2 的定义不在该 RFC 的范围内。它不是一个等待未来发明的神秘算法,其详细规则可以追溯到 DCE Security 规范。

5.1 位结构

v2 以 v1 为基础,将 time_low 替换为本地标识符,将 clock_seq_low 替换为 Domain:

1
2
3
UUIDv2 / DCE Security(总计 128 bit)
[local_id:32][time_mid:16][ver=0010:4][time_high:12]
[var=10:2][clock_seq_high:6][local_domain:8][node:48]
字段 位数 作用
local_id 32 指定 Domain 内的本地 ID,例如 POSIX UID、GID
time_mid 16 保留的 Gregorian 时间中间字段
time_high 12 保留的 Gregorian 时间高位字段
version 4 固定为 2
variant 2 固定 10
clock_seq_high 6 时钟序列的剩余部分
local_domain 8 标识本地 ID 的类别
node 48 节点标识

常见的本地 Domain 包括:

Domain 含义 本地 ID 的例子
0 Person POSIX 用户 UID
1 Group POSIX 用户组 GID
2 Organization 组织或站点定义的标识

5.2 生成规则

在 v1 时间型 UUID 的基础上,选择 Local Domain 和对应的 32 位 Local ID,覆盖原先的 time_low,再把 Domain 编码进 clock_seq_low,Version 设置为 2。由于时间戳最低 32 位被覆盖,v2 不再保有 v1 的精细时间信息;按原时间刻度估算,被覆盖的范围相当于约 429.5 秒。

这也解释了为什么 v2 不适合被视作普通的高精度时间型 ID。

适合:确实需要 DCE Security 身份语义的历史/专用系统。

不建议:普通用户主键、权限系统中的真实授权凭据、业务订单 ID。UUIDv2 中含有 UID/GID 信息并不代表持有该 UUID 就经过身份认证。

六、UUIDv3:基于 MD5 的名称型 UUID

6.1 位结构

1
2
UUIDv3(总计 128 bit)
[md5_high:48][ver=0011:4][md5_mid:12][var=10:2][md5_low:62]

v3 与 v1 看起来一样长,但其余 122 位是从摘要中得到的,不再对应时间、时钟序列和 MAC。

6.2 生成规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Namespace UUID(16 字节)
+
Name(按命名空间规则编码为字节)
│
▼
MD5(NamespaceBytes || NameBytes)
│
▼
16 字节摘要
│
├── 覆盖 Version = 3
└── 覆盖 Variant = 10
│
▼
UUIDv3

同一个 Namespace、同一个规范化后的 Name,会稳定得到同一个 UUID;改动任一输入,结果一般会完全变化。RFC 定义了 DNS、URL、OID、X.500 DN 等命名空间标识。

例如,按标准 DNS Namespace 生成 example.com 的 v3 UUID:

1
9073926b-929f-31c2-abc9-fad77ae3e8eb

适合:必须兼容历史 v3 生成结果的系统、既有 MD5 命名空间数据。

不建议:新项目中的默认名称型算法。MD5 已不适合抵抗有意构造的碰撞,更不能把名称型 UUID 当作签名或认证结果。

七、UUIDv4:最常见的随机型 UUID

7.1 位结构

1
2
3
4
UUIDv4(总计 128 bit)
[random_a:48][ver=0100:4][random_b:12][var=10:2][random_c:62]

可使用的随机位数 = 128 - 4 - 2 = 122 位

7.2 生成规则

  1. 从加密安全随机数生成器(CSPRNG)取得随机字节。
  2. 将版本所在 4 位覆写为 0100。
  3. 将变体高两位覆写为 10。
  4. 将最终 128 位按标准格式输出。

UUIDv4 的核心是 122 位随机空间,而非依赖 MAC、机器编号或时间戳。其碰撞概率可用生日悖论近似:

1
P(至少一次碰撞) ≈ n(n - 1) / (2 × 2^122)

其中 n 是生成次数,并假设随机数独立、均匀。如果累计生成 10^12 个 v4 UUID,理论近似碰撞概率约为 9.4 × 10^-14。这个概率非常低,但不是零;随机数质量不佳或错误复用种子会让实际风险远高于该估算。

7.3 推荐场景

v4 适用于应用对象 ID、跨服务关联 ID、普通分布式标识等无需时间顺序的场景,尤其是在生成端之间没有协调机制时。

其主要弱点不是全局冲突风险,而是无序写入数据库索引:当它被用作 InnoDB 聚簇主键时,插入位置散布在索引树的不同叶子页,容易带来额外页读取、页分裂与缓存压力。

推荐:普通随机标识;不以 UUID 为聚簇主键的系统。

审慎使用:超大表、高写入压力的 InnoDB 主键。需要主键插入局部性时,更应评估 v7、BIGINT 或其他有序方案。

v4 采用 CSPRNG 时可能具有不错的不可猜测性,但 UUID 仍只是标识符,不应直接代替认证令牌、访问控制或服务端权限检查。

八、UUIDv5:基于 SHA-1 的名称型 UUID

8.1 位结构

1
2
UUIDv5(总计 128 bit)
[sha1_high:48][ver=0101:4][sha1_mid:12][var=10:2][sha1_low:62]

v5 与 v3 的字段形状相同,主要区别是哈希算法。

8.2 生成规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NamespaceBytes || NameBytes
│
▼
SHA-1
│
▼
160 位哈希结果
│
▼
取最左边 128 位
│
├── 覆写 Version = 5
└── 覆写 Variant = 10
│
▼
UUIDv5

仍以 DNS Namespace 下的 example.com 为例,标准 v5 结果是:

1
cfbff0d1-9375-5685-968c-48ce8b15ae17

名称型算法最大的价值是可复现性:不依赖中心发号器,只要参与方采用同一个 Namespace 和完全一致的名称字节序列,就能得到同一个 ID。

实际应用中需要特别约定名称的编码、大小写、Unicode 归一化、URL 格式与是否去除尾部斜杠。即使两个字符串在业务上表示同一对象,只要参与哈希的字节不同,通常就会生成不同 UUID。

8.3 推荐场景

适合:稳定的外部资源映射、DNS/URL 名称映射、跨环境同步时可重算的对象标识、数据导入去重键(前提是业务名称规则稳定且唯一)。

不适合:本来就允许同名的不同对象、需要真正随机且不可推断的标识、对抗性碰撞安全要求严格的摘要身份机制。

RFC 9562 推荐在 v3/v5 之间优先使用 v5;但 SHA-1 也已经不适合新的密码学安全需求。如果必须采用 SHA-256 等更新的哈希算法,不要把 SHA-256 的结果伪装成 v5,而应遵循自定义 UUIDv8 的约定。

九、UUIDv6:给 v1 重新排列时间字段

v6 是为了改善 v1 的数据库局部性而设计的时间型格式,可以理解为“保留 v1 语义,调整时间字段排序”。

9.1 位结构

1
2
3
4
5
UUIDv6(总计 128 bit)
[time_high:32][time_mid:16][ver=0110:4][time_low:12][var=10:2][clock_seq:14][node:48]

时间戳按从高位到低位排列:
T(60bit) = [time_high:32][time_mid:16][time_low:12]

与 v1 放在一起看:

版本 60 位时间戳在 UUID 中的排列 索引特点
v1 低 32 → 中 16 → 高 12 时间低位先变化,跨时间范围排序不自然
v6 高 32 → 中 16 → 低 12 高位在前,比较结果更接近生成时间

9.2 生成规则

  1. 获取 v1 同源的 60 位 Gregorian 时间戳。
  2. 按 高 32 位 + 中 16 位 + 低 12 位 的顺序放入 UUID。
  3. 保留 14 位时钟序列与 48 位节点标识字段的位置。
  4. 固定 Version 为 6,Variant 为 10。

RFC 9562 建议新生成的 v6 对 Clock Sequence 与 Node 使用伪随机值,但为了兼容已有系统,也允许沿用 v1 的时钟序列/MAC 使用模式。

9.3 推荐场景

适合:既有 v1 时间体系的改造、需要使用 Gregorian 时间结构且希望更好排序的旧系统。

不建议:没有 v1 历史包袱的新业务。RFC 9562 明确提出,不涉及历史 v1 的系统通常应优先采用 v7。

十、UUIDv7:面向现代分布式数据库的时间有序 UUID

v7 是本次标准演进中最值得工程实践关注的版本。它直接采用 Unix 毫秒时间戳,并把该时间戳放在 UUID 的最高 48 位。

10.1 位结构

1
2
3
4
5
6
7
8
9
UUIDv7(总计 128 bit)
[unix_ts_ms:48][ver=0111:4][rand_a:12][var=10:2][rand_b:62]

┌──────────────────────┬────────┬────────────┬───────┬──────────────────────┐
│ Unix 毫秒时间戳 │ Version│ rand_a │Variant│ rand_b │
│ 48 bit │ 4 bit │ 12 bit │ 2 bit │ 62 bit │
└──────────────────────┴────────┴────────────┴───────┴──────────────────────┘

除时间/版本/变体外,剩余共 74 位:rand_a 12 + rand_b 62。
字段 位数 作用
unix_ts_ms 48 Unix Epoch 之后经过的毫秒数,网络字节序
version 4 固定 0111
rand_a 12 随机值,或单毫秒内的可选子毫秒信息/计数器的一部分
variant 2 固定 10
rand_b 62 随机值或可选单调计数器的剩余部分

可以把 v7 的前 48 位看作可直接读取的时间前缀。按规范的字节序对 UUID 排序时,不同毫秒产生的 UUID 通常自然聚集在一起。

10.2 生成规则:纯随机型与单调增强型

规范允许至少两类实现策略,而不是要求大家使用完全相同的计数器算法。

策略 A:74 位全部用随机数据填充。

1
2
3
4
5
6
7
8
9
10
11
读取当前 Unix 毫秒时间戳
│
▼
写入高 48 位时间戳
│
├── Version = 7
├── Variant = 10
└── 剩余 74 位填入 CSPRNG 随机数
│
▼
输出 UUIDv7

这个方案简单、分布式节点之间不用协商。但同一毫秒内生成多个 UUID 时,低位是随机的,不能保证同一毫秒内按生成先后严格递增。

策略 B:使用可选的子毫秒精度或计数器增强单调性。

RFC 9562 允许按顺序在剩余 74 位中放置:

  1. 可选的子毫秒时间分数(最多 12 位);
  2. 可选的、正确初始化和维护的计数器;
  3. 仍未使用的位填入随机数据。

生成器可以在同一毫秒内让计数器递增,以满足单个生成器内部的更强排序要求;同时必须考虑时钟回拨、计数器溢出、并发同步和跨进程状态。

维度 纯随机型 v7 单调增强型 v7
跨毫秒排序 通常有序 通常有序
同毫秒内严格递增 不保证 实现正确时,可在单个生成器范围内保证
分布式节点协调 不需要 不需要统一发号,但各节点不能天然共享全局次序
随机性 74 位随机 部分位用于计数/精度,随机熵减少
实现复杂度 低 更高,需要处理回拨与溢出

10.3 v7 为什么更适合数据库索引?

B-Tree/B+Tree 按键值组织叶子页。随机 v4 主键可能持续向很多不同的叶子页插入;v7 把时间放在前面,让新数据更可能落到相邻的键范围。

1
2
3
4
5
UUIDv4(随机主键)
新插入记录 → [页 3]、[页 81]、[页 11]、[页 46] ...

UUIDv7(时间有序主键)
新插入记录 → [近期时间段的页] → [相邻页] → ...

这种局部性通常意味着较少的随机页面访问、较好的缓存命中率,以及可能更低的页分裂成本。实际收益取决于写入速率、索引组织、并发热点、页大小与数据库实现,需要基准测试验证。

但必须区分三个容易混淆的结论:

  • v7 是时间有序的,不是严格全局单调的。 多节点时钟偏差、同毫秒随机排序、消息延迟都可能让 ID 顺序与真实业务发生顺序不同。
  • v7 自带时间,并不意味着可以删掉 created_at。 审计、业务生效时间、延迟写入、回填数据、更新排序、时间修正依旧需要独立时间字段。
  • v7 并不保证绝对不碰撞。 规范要求依赖足够质量的随机源或正确设计的计数状态,数据库仍然应设置主键/唯一约束。

10.4 推荐场景

优先考虑:分布式订单主键、支付流水标识、异步事件 ID、日志事件 ID、大量新增写入的数据库表(在业务需要使用 UUID 的前提下)。

不一定适合:要求隐藏大致创建时间的公开 ID、必须有跨节点全序的业务序列号、对主键存储成本极敏感且可使用 BIGINT 的表。

若排序本身就是业务规则,应另外使用可信的时间列、数据库提交顺序、消息偏移量或专门的序列机制,不能仅依赖 v7 的字典序。

十一、UUIDv8:自定义的 122 位空间

v8 是标准预留的实验性/厂商专用 UUID 格式,不是 v7 的功能升级版。

11.1 位结构

1
2
3
4
UUIDv8(总计 128 bit)
[custom_a:48][ver=1000:4][custom_b:12][var=10:2][custom_c:62]

可由应用自行定义的字段合计:48 + 12 + 62 = 122 位

唯一强制遵守的结构性要求是:Version 必须为 8,Variant 必须为 10。其余 122 位由应用自定义,标准不保证这些自定义内容的唯一性、时间顺序或安全性。

11.2 生成规则与一个设计例子

假设某企业需要按以下逻辑生成标识:

1
2
3
4
5
6
7
8
9
10
11
12
13
业务 Namespace + 规范化后的业务编码
│
▼
SHA-256
│
▼
取 128 位
│
├── Version = 8
└── Variant = 10
│
▼
UUIDv8

这是一个可以设计的 v8 方案方向,并非所有 v8 UUID 都使用 SHA-256,也不代表只写这几行就完成了一个安全协议。生产使用时还需要固定域分离、编码方式、取位规则、版本管理、碰撞处理和测试向量。RFC 9562 附录 B 也提供了 SHA-256 名称型 UUIDv8 的说明性样例。

适合:已有明确位布局协议约束、跨企业约定的自定义 ID、需要比 SHA-1 更新的名称型 UUID 算法。

不建议:只是为了“以后扩展方便”就自创一个 UUID 方案。自定义格式会增加 SDK、数据库和其他团队的互操作成本。

十二、Nil UUID、Max UUID 和常见边界

除了 v1~v8,RFC 9562 还明确了两个特殊值:

特殊 UUID 内容 用途
Nil UUID 00000000-0000-0000-0000-000000000000 表示空、缺省或不存在的 UUID 值
Max UUID ffffffff-ffff-ffff-ffff-ffffffffffff 作为最大值哨兵、范围边界等,取决于应用约定

它们不是 UUIDv0 和 UUIDv15。其 Variant 也不属于前述普通 RFC 10 格式,不能拿“第三组是否是 1~8、第四组是否是 8~b”的普通 UUID 检查规则否定它们的存在。

Nil/Max 可以作为约定的哨兵值,但要明确语义。业务中通常更推荐数据库 NULL 表示真正的缺失值,避免哨兵值与有效数据混淆。

十三、Java 实战:v3、v4、v5、v7 怎么生成?

13.1 JDK 自带能力与局限

截至 JDK 25,java.util.UUID 的常用静态工厂方法是:

Java API 生成版本 注意点
UUID.randomUUID() v4 使用加密强度较高的伪随机数生成器
UUID.nameUUIDFromBytes(bytes) v3 基于传入字节的 MD5;不会自动帮你加入 RFC Namespace
UUID.fromString(text) 解析 只解析表示形式,不生成新 ID
uuid.version() 检测 返回 Version,Nil/Max 等特殊值不可按普通生成版本解读
uuid.variant() 检测 RFC 10 变体在 Java 中返回 2

JDK 自带的 UUID.randomUUID() 不会因为项目升级到 JDK 21 或 JDK 25 就自动生成 v7。

13.2 使用 JDK 生成 v3、v4、v5,并演示 v7 位布局

下面是一份可独立编译运行的 Java 示例。v3/v5 显式将标准 Namespace UUID 的 16 字节与 UTF-8 名称拼接;v7 以纯随机方案展示 RFC 9562 的字段填充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.UUID;

public class UuidExamples {
private static final SecureRandom RANDOM = new SecureRandom();
private static final UUID NS_DNS = UUID.fromString(
"6ba7b810-9dad-11d1-80b4-00c04fd430c8");

public static UUID v3(UUID namespace, String name) {
return UUID.nameUUIDFromBytes(namespacedBytes(namespace, name));
}

public static UUID v5(UUID namespace, String name) {
byte[] hash;
try {
hash = MessageDigest.getInstance("SHA-1")
.digest(namespacedBytes(namespace, name));
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("JDK SHA-1 unavailable", e);
}
// SHA-1 的前 16 字节构成 UUID 的基础,再覆盖版本/变体位。
hash[6] = (byte) ((hash[6] & 0x0f) | 0x50);
hash[8] = (byte) ((hash[8] & 0x3f) | 0x80);
ByteBuffer bytes = ByteBuffer.wrap(hash, 0, 16);
return new UUID(bytes.getLong(), bytes.getLong());
}

private static byte[] namespacedBytes(UUID namespace, String name) {
byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8);
ByteBuffer bytes = ByteBuffer.allocate(16 + nameBytes.length);
bytes.putLong(namespace.getMostSignificantBits());
bytes.putLong(namespace.getLeastSignificantBits());
bytes.put(nameBytes);
return bytes.array();
}

// 教学版:合法的纯随机型 UUIDv7;不保证同一毫秒严格递增。
public static UUID v7Demo() {
long millis = System.currentTimeMillis();
long msb = ((millis & 0xFFFFFFFFFFFFL) << 16)
| 0x7000L | RANDOM.nextInt(1 << 12);
long lsb = (RANDOM.nextLong() & 0x3FFFFFFFFFFFFFFFL)
| 0x8000000000000000L;
return new UUID(msb, lsb);
}

public static Instant v7Timestamp(UUID uuid) {
if (uuid.variant() != 2 || uuid.version() != 7) {
throw new IllegalArgumentException("Not an RFC UUIDv7");
}
return Instant.ofEpochMilli(uuid.getMostSignificantBits() >>> 16);
}

public static void main(String[] args) {
System.out.println("v3 = " + v3(NS_DNS, "example.com"));
System.out.println("v5 = " + v5(NS_DNS, "example.com"));
System.out.println("v4 = " + UUID.randomUUID());
UUID id = v7Demo();
System.out.println("v7 = " + id);
System.out.println("version = " + id.version());
System.out.println("variant = " + id.variant());
System.out.println("createdAt = " + v7Timestamp(id));
}
}

前两个确定性 UUID 的输出应为:

1
2
v3 = 9073926b-929f-31c2-abc9-fad77ae3e8eb
v5 = cfbff0d1-9375-5685-968c-48ce8b15ae17

这个 v7 方法适用于理解位布局与验证算法,不建议把教学代码直接作为高并发生产级 ID 组件。例如,它没有加入单毫秒计数器、回拨保护、持久化状态或吞吐压测。生产环境宜使用经过测试的 UUIDv7 实现,并根据业务选择随机型或单调型生成策略。

13.3 Java 工程中使用 uuid-creator

开源项目 uuid-creator 提供了各版本的生成方法。以下 Maven 版本按本文编写时核对的项目文档固定为 6.1.1:

1
2
3
4
5
<dependency>
<groupId>com.github.f4b6a3</groupId>
<artifactId>uuid-creator</artifactId>
<version>6.1.1</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import com.github.f4b6a3.uuid.UuidCreator;
import com.github.f4b6a3.uuid.enums.UuidLocalDomain;
import com.github.f4b6a3.uuid.enums.UuidNamespace;
import java.util.UUID;

public class UuidCreatorDemo {
public static void main(String[] args) {
UUID v1 = UuidCreator.getTimeBased();
UUID v2 = UuidCreator.getDceSecurity(
UuidLocalDomain.LOCAL_DOMAIN_PERSON, 1001);
UUID v3 = UuidCreator.getNameBasedMd5(
UuidNamespace.NAMESPACE_DNS, "example.com");
UUID v4 = UuidCreator.getRandomBased();
UUID v5 = UuidCreator.getNameBasedSha1(
UuidNamespace.NAMESPACE_DNS, "example.com");
UUID v6 = UuidCreator.getTimeOrdered();
UUID v7 = UuidCreator.getTimeOrderedEpoch();

System.out.println(v1);
System.out.println(v2);
System.out.println(v3);
System.out.println(v4);
System.out.println(v5);
System.out.println(v6);
System.out.println(v7);
}
}

这里的 v7 方法在当前库中有自己的计数器策略,可按项目文档进一步选用其他单调生成方法。需要注意,“对生成器内部有序”不意味着跨机器严格排序。

十四、数据库实战:MySQL 与 PostgreSQL

14.1 为什么不建议直接把 UUID 主键定义为 CHAR(36)?

UUID 的本体只有 16 字节,但规范文本形式包含 36 个字符。两者相比:

列类型 表达 UUID 的方式 标识本体所需字节数 特点
CHAR(36) 36 字符(包含连字符) ASCII/UTF-8 文本约 36 字节 肉眼可读,但索引键更宽;实际存储还受字符集和行格式影响
BINARY(16) 16 字节原始值 16 字节 MySQL 中通常更适合建立索引,需做显示格式转换
PostgreSQL uuid 原生 UUID 类型 16 字节数据 数据库原生解析、比较与索引支持
BIGINT 64 位整数 8 字节 更紧凑,但不是 UUID,分布式唯一性需要额外算法约束

尤其在 InnoDB 中,二级索引记录会携带聚簇主键列。较宽的主键不只影响主键索引,还会增加相关二级索引的存储成本。所以是否采用 UUID,不应只看“省掉发号器”这一项收益。

14.2 MySQL:v7 建议按原始顺序存入 BINARY(16)

下面以一个业务订单表为例:

1
2
3
4
5
6
7
8
9
CREATE TABLE order_record (
id BINARY(16) NOT NULL COMMENT 'UUIDv7,按标准字节序存放',
order_no VARCHAR(32) NOT NULL,
amount DECIMAL(18, 2) NOT NULL,
gmt_create DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_order_no (order_no),
KEY idx_gmt_create (gmt_create)
) ENGINE = InnoDB;

应用层生成 UUIDv7 后,将字符串按顺序转换为 16 字节:

1
2
3
4
5
6
7
8
9
10
11
12
-- ? 参数由应用传入合法 UUIDv7 字符串
INSERT INTO order_record (id, order_no, amount)
VALUES (UUID_TO_BIN(?, 0), ?, ?);

-- 查询时转回规范字符串
SELECT BIN_TO_UUID(id, 0) AS id,
order_no,
amount,
gmt_create
FROM order_record
ORDER BY id DESC
LIMIT 20;

这里 UUID_TO_BIN(uuid, 0) 的 0 表示不交换字节顺序。

一个很容易踩的坑:UUID_TO_BIN(uuid, 1) 不是给 v7 用的

MySQL 的 UUID_TO_BIN(value, 1) 会交换 UUID 的时间字段,以改善 v1 值在索引中的排列。官方文档明确指出该优化假定输入是 UUIDv1。

输入版本 建议转换方式 说明
v1 UUID_TO_BIN(id, 1) 通过 MySQL 特定交换方式改善其时间部分的顺序;读取时要对应使用 BIN_TO_UUID(id, 1)
v4 UUID_TO_BIN(id, 0) 无时间字段可交换,保持标准顺序
v6 UUID_TO_BIN(id, 0) 本来就是高位时间在前,不要再按 v1 规则交换
v7 UUID_TO_BIN(id, 0) 时间已经位于最前 48 位,交换会破坏预期时间顺序
v3/v5/v8 UUID_TO_BIN(id, 0) 通常保留规范字节序,自定义 v8 另按协议约定
1
2
3
4
5
-- 这是 v1 风格的做法,不能机械搬到 v7:
SELECT UUID_TO_BIN(UUID(), 1);

-- 对于应用生成的 v7,应保留标准顺序:
SELECT UUID_TO_BIN('019535d9-3df7-79fb-b466-fa907fa17f9e', 0);

还需要注意,MySQL 的 UUID() 本身产生的是 v1 UUID,不是 v4,更不是 v7。不要因为用了 UUID_TO_BIN() 就误以为数据库函数会自动升级 UUID 版本。

14.3 PostgreSQL 18:原生生成 v7

从 PostgreSQL 18 开始,数据库官方提供了 uuidv7():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TABLE order_record (
id UUID PRIMARY KEY DEFAULT uuidv7(),
order_no TEXT NOT NULL UNIQUE,
amount NUMERIC(18, 2) NOT NULL,
gmt_create TIMESTAMPTZ NOT NULL DEFAULT now()
);

INSERT INTO order_record(order_no, amount)
VALUES ('SO202610140001', 99.00);

SELECT id, order_no, gmt_create
FROM order_record
ORDER BY id DESC
LIMIT 20;

PostgreSQL 的 uuid 类型本身可以存放多种 UUID 版本;但 uuidv7() 的内建生成能力是 PostgreSQL 18 才引入的,不能照搬到 16/17 并期待内建函数存在。旧版本可以在应用层或经审查的扩展中生成 v7,再写入 uuid 列。

14.4 是否应该用 v7 替换所有自增主键?

维度 BIGINT 自增 UUIDv4 UUIDv7
主键宽度 8 字节 16 字节 16 字节
多节点无协调生成 通常不具备 支持 支持
大致按生成时间排列 单实例序列递增,但不直接代表时间 不支持 支持
B-Tree 插入局部性 通常很好 通常较差 通常较好
是否暴露大致生成时间 ID 本身未显式编码时间 不暴露 暴露
依赖中心数据库发号 常见 否 否
典型场景 单库/紧凑内部主键 普通随机标识 分布式可排序 UUID 主键

如果所有写入都走同一个 MySQL 实例,自增 BIGINT 的简单、紧凑和成熟仍有价值;如果多服务必须独立生成主键,UUIDv7 是有吸引力的替代方案。两者的取舍必须把网络协调成本、索引成本、主键大小、数据迁移成本和业务暴露要求一起衡量。

十五、工程设计中容易忽略的六个问题

误区 更准确的理解
“UUID 一定不会重复” UUID 的唯一性依赖版本算法、节点状态或随机质量;应用仍须维护唯一约束和冲突处理策略
“Version 数字越大就越好” Version 是不同生成算法的编号;v8 不会天然优于 v7,v7 也不适合所有场景
“UUIDv7 严格全局递增” 标准只提供时间高位布局;单调性与跨节点有序性取决于实现、时间同步和范围
“UUIDv7 有时间戳,可以删掉创建时间列” ID 时间是生成时刻线索,无法代替审计、业务创建/提交时间或校正后的时间字段
“名称型 UUID 可以作为摘要签名” v3/v5 的主要目标是确定性命名,而不是密码学身份认证;对抗性散列场景需单独设计安全方案
“MySQL UUID_TO_BIN 的 swap=1 永远更快” swap=1 针对 v1 时间字段优化;v6/v7 应按原始网络字节序存放

还有一些具体工程边界值得提前确定:

  • 时钟与多节点:v7 依赖系统时间。节点时钟明显回拨或时钟不同步时,业务上“后写入”未必对应“更大的 UUID”。
  • 容量与索引:UUID 的 128 位不会因为改用 BINARY(16) 而变成 64 位;二级索引依旧受到较宽主键影响。
  • 安全与隐私:v1/v2/v6 的 Node 可能携带设备信息,v7 公开了生成时间;v4、v5 的用途也不等于安全令牌。
  • 跨语言序列化:Java、Go、Python、SQL、Windows GUID 等生态之间交换 16 字节时,要统一规范字节序,不能只保证文本看起来一致。
  • 业务去重:订单号、请求幂等键、业务自然键与 UUID 主键是不同概念。UUID 本身不能阻止重复下单。
  • 版本升级:旧表从 v4 迁到 v7 不一定要重写全部历史 ID,但混合版本的全列排序不再等价于时间排序;迁移前应评估主键与外键约束。

十六、版本选型决策图与学习路线

下图用于快速判断生成方式,而不是声称所有系统都必须使用 UUID。Hexo 主题若未启用 Mermaid 渲染插件,图形代码块会按普通代码展示,下面的对比表仍可独立阅读:

flowchart TD
    A[需要生成一个标识符] --> B{必须采用 UUID 吗?}
    B -->|否| C[评估 BIGINT / 序列 / Snowflake 等]
    B -->|是| D{相同名称是否必须生成相同 ID?}
    D -->|是| E{需要自定义哈希或字段吗?}
    E -->|否| F[UUIDv5]
    E -->|是| G[UUIDv8 + 明确协议]
    D -->|否| H{是否需要时间有序/索引局部性?}
    H -->|否| I[UUIDv4]
    H -->|是| J{需要兼容历史 v1 格式吗?}
    J -->|是| K[UUIDv6]
    J -->|否| L[UUIDv7]

学习时可以按照下面的顺序建立理解,不必机械按 v1、v2、v3……依次死记:

阶段 重点 应该掌握什么
入门 UUID 128 位格式、Version、Variant 会读一个 UUID,知道 8-4-4-4-12 和固定控制位
基础 v4 与 v5 理解随机唯一性和名称确定性是两种不同需求
原理 v1 与 v6 理解 Gregorian 时间、Clock Sequence、Node、时间字段重排
工程重点 v7 理解 48 位时间戳、74 位剩余数据、同毫秒单调性与数据库局部性
历史兼容 v2 与 v3 了解 DCE 身份场景和 MD5 名称生成的兼容价值
高级扩展 v8 能设计并评审自定义位布局、碰撞模型和跨语言协议
落地 Java + MySQL/PostgreSQL 正确生成、序列化、存储、索引与压测

最后再用一张表记住结构

1
2
3
4
5
6
7
8
9
10
11
12
所有行都共 128 位;下列写法按从最高位到最低位排列:

v1 : [时间低32] [时间中16] [ver1|时间高12] [var|时钟序列14] [节点48]
v2 : [本地ID32] [时间中16] [ver2|时间高12] [var|时钟序列6|Domain8] [节点48]
v3 : [MD5高48] [ver3|MD5中12] [var|MD5低62]
v4 : [随机48] [ver4|随机12] [var|随机62]
v5 : [SHA1高48] [ver5|SHA1中12] [var|SHA1低62]
v6 : [时间高32] [时间中16] [ver6|时间低12] [var|时钟序列14] [节点48]
v7 : [Unix毫秒48] [ver7|随机/计数12] [var|随机/计数62]
v8 : [自定义48] [ver8|自定义12] [var|自定义62]

ver = 4 bit var = 2 bit(RFC 标准变体,固定为 10)

总结

UUID 解决的首先是“不依赖中心发号器,如何生成低冲突概率、可跨系统使用的标识”;不同版本又针对可复现性、时间结构、索引局部性和自定义协议给出了不同答案。

从工程选择来看,最常用的并不是八种版本各取一点,而是围绕三个问题做取舍:是否需要随机标识、是否需要名称确定性、是否需要时间有序。 对应的默认选项分别是 v4、v5 和 v7。v1/v6 解决历史时间型格式及兼容问题,v2 是 DCE 专用身份格式,v3 留给老系统,v8 则为真正有协议设计需求的场景保留空间。

对于新的 Java 分布式服务,如果需要使用 UUID 作为 MySQL 主键,我会优先评估 UUIDv7 + BINARY(16) 标准字节序存储 + 唯一约束 + 独立创建时间列。它不一定比 BIGINT 更轻,但相较无序 v4 主键,通常是更符合数据库写入特性的 UUID 选择。

参考资料

本次提供的四篇资料

  1. Nicole Tietz-Sokolskaya, TIL: 8 versions of UUID and when to use them. 原文
  2. IETF, RFC 4122: A Universally Unique IDentifier (UUID) URN Namespace(2005;已由 RFC 9562 取代). RFC Editor
  3. 博客园 -云-,深度解读 UUID:结构、原理以及生成机制. 原文
  4. CSDN,UUID 的 8 个版本以及何时使用它们. 原文

补充核对的规范与官方文档

  1. IETF, RFC 9562: Universally Unique IDentifiers (UUIDs)(2024,现行标准). RFC 原文
  2. The Open Group, DCE 1.1 / DCE Security(UUIDv2 的历史规范背景). DCE UUID 规范入口
  3. Oracle, MySQL Reference Manual — UUID(), UUID_TO_BIN(), BIN_TO_UUID(). 官方手册
  4. PostgreSQL, UUID Functions (PostgreSQL 18). 官方手册
  5. Oracle, Java SE 25 — java.util.UUID. JDK API
  6. Fabio Lima, uuid-creator. 项目仓库及示例

UUID 完全指南:从 RFC 4122 到 RFC 9562,理解 v1~v8 的结构、生成算法与工程选型
https://allendericdalexander.github.io/2026/10/14/archtect/db/UUID/
作者
AtLuoFu
发布于
2026年10月14日
许可协议