记一次临时起意的UUID探索及v7的Java实现
事情是这样——
由于工作原因,和项目组的小伙伴协调唯一标识符的方案,最终选择使用UUID v7。
这使用Java实现起来当然很简单,甚至可以说是基本功。
但是作为野路子起家的人,我还并未真正深入了解过UUID发展史。
于是,在一个闲来无聊的夜晚,我稍微研究了一下,并写下了这篇文章——
UUID是什么?
开始之前问一个问题:
你在构建分布式系统时,是否为如何生成全局唯一的标识符而感到苦恼?
UUID正是为了解决这一痛点问题而诞生的。
UUID即Universally Unique Identifier的缩写,它的大名叫通用唯一识别码。在全球互联网化的今天,它的贡献功不可没。
作为使用128位数据,由16个8位字节构成的标识符,它的碰撞概率低到完全可以被认为是具有唯一性的。
我们可以使用生日悖论(Birthday Paradox) 来对其碰撞概率进行计算:
生日悖论
先来思考一个问题:
在一个房间里,有多少人时,有超过50%的概率,至少有两个人生日相同?
很多人会下意识的以为需要100多个人才可能出现重复的生日,但是如果我说
答案是23个人呢?
我们先来做一下基本假设,在这个房间内——
每年的生日均匀分布(忽略闰年等特殊情况)
每个人生日相互独立
做好了基本假设,我们我们用小学二年级的知识便可以开始进行计算。
设房间内有$n$个人,那么我们可以计算 "没有任何两人生日相同的概率",然后用1减去这个概率,即
我们用阶乘可以写成如下形式:
即:
注意到当$n = 23$时:
问题迎刃而解。
生日悖论在UUID v4碰撞概率推算中的应用
我们选用最为经典的UUID v4来进行碰撞概率推算。
上文中我们提到:
UUID使用128位数据
UUID v4同样作为使用128位数据生成的内容,它有$2^{128}$种可能,约为:
设我们随机生成$n$个UUID,求这$n$个中至少有两个重复的概率。
同样,我们计算全部不同的概率,并用1减去它。
使用生日悖论的近似公式可表示为:
设$P_{\text{重复}} = 0.5$,求解$n$:
因为$n(n - 1) \approx n^2$当$n$很大时,得:
即要使UUID v4的碰撞概率超过50%,需要生成约$1.6 \times 10^{19}$个UUID,更不用提基于时间戳计算得出的UUID v7等后续改进版本了。
所以我们完全可以认定它具有唯一性。
有点听累了?没关系,我们来看点轻松的——
UUID发展史
上世纪80年代,Apollo公司的软件工程师Roedy Green在1987年首次提出了UUID。
为什么Roedy Green要提出这么一种东西?
Apollo作为最早进军工作站市场的公司之一,深刻的意识工作站与大型机的鲜明对比,分布式计算的理念将成为主流。

Apollo早期的工作站宣传页
为了识别这么多设备,我们当然需要一种统一的标准来帮助我们完成这件事。
NCS(网络计算系统)最早引入了UID(通用标识符)的概念,作为唯一识别身份。UID是一组64位数字,将单调时钟与永久嵌入其所有工作站硬件中的唯一主机ID相结合。在这种方案下,标识符可以在每台主机上每秒生成数千次,并始终保持全局唯一,没有扩展瓶颈。
而唯一的问题发生在了Apollo——在那里,一个标识符会永远跟随一台设备。
很显然,当Apollo开始为NCA(网络计算架构)引入NCS的这一标准时,UID数量多少有点捉襟见肘。
根据历史的车轮,我们都能猜到,UUID诞生了。
第一个UUID
在分布式系统中,不同的计算节点都可能需要创建资源、对象或文件等实体,而这些实体需要有唯一的标识符。为了避免不同机器之间命名冲突,Roedy Green设计了一种无中心协调的唯一标识符生成方法,这就是UUID的雏形。
UUID建立在UID的基础之上,在拓展空间为128位后,Roedy Green又提出了一些核心思想来帮助UUID更为完善和唯一。
Roedy Green的设计思路包含以下核心思想:
利用每台机器唯一的MAC地址。
利用当前的时间戳(精确到100纳秒)。
添加随机数或序列号以避免冲突。
生成128位的标识符,确保在天文数量级创建ID的情况下,也不会重复。
而他的这一思想,后来也成为了UUID v1的核心。
第一个UUID没有被正式记录下来,因为它最初是由Apollo公司内部系统自动生成的,并没有像今天这样被标准化或归档。
但也许我们可以简单还原出来一个典型的"第一个 UUID"长什么样子?
让我们先了解一下UUID v1的格式结构:
| 字段 | 大小 | 描述 |
|---|---|---|
time-low | 32 | 时间戳低位 |
time-mid | 16 | 时间戳中位 |
time-high-and-version | 16 | 时间戳高位和UUID版本号 |
clock-seq-and-reserved | 8 | 时钟序列高位和保留位(variant 标志) |
clock-seq-low | 8 | 时钟序列低位 |
node | 48 | 设备MAC地址,也可能是个伪随机数 |
我们再来详细了解一下里面的内容:
UUID v1的时间戳是从1582年10月15日00:00:00UTC(即格里高利历的起始时间,为了最大限度避免与Unix时间重叠,并避免冲突)到现在所经过的100纳秒为单位的计数值,总大小为60,分布在:
time-low32time-mid16time-high-and-version前12
UUID版本号即为字面意思,在UUID v1内固定为1,以此类推。
时钟序列总大小为14,用于在系统时间发生倒退或设备重启时防止UUID重复,分布在:
clock-seq-and-reserved前6clock-seq-low8
variant 标识包含在
clock-seq-and-reserved中,用于标志该UUID遵循的标准。UUID v1遵循RFC 4122,使用10。node使用的设备MAC地址可以保证设备唯一性,但如果没有可用的MAC地址,拿伪随机数来凑合凑合也不是不可以。
很简单,不是吗?
我们可以根据上面的格式尝试还原出来一个典型的"第一个 UUID"的大概样子——
f81d4fae-7dec-11d0-a765-00a0c91e6bf6
这是一个历史上著名的UUID示例,它来自 Microsoft的DCE示例。
深挖完UUID的开家史,我们再来跟随历史的车轮往前看。
黏附在历史车轮上的UUID
历史车轮滚滚向前,UUID因其特性在全球范围开始大范围应用。
2005年,UUID由开放软件基金会(Open Software Foundation)定义,并标准化为RFC 4122。2024 年,RFC 9562推出了另外三个UUID版本——6、7、8以解决早期版本的局限性。
在此期间,UUID v2发布,这是一个很冷门的版本,以至于很多UUID介绍中遗漏了这个版本。Wikipedia中这样写道:
RFC 4122 保留了版本2的UUID用于“DCE security”;但并没有提供任何细节。因此,许多 UUID 实现省略了“版本2”。
仔细想来其实也并非是前人在写文时的刻意遗漏,而是UUID v2的变化和解决的问题实在有限。
UUID v2与UUID v1十分相像,除了时钟序列中clock-seq-low被local-main替换,也就剩下time-low由在指定本地域内有意义的整数标识符替换。
哦,对了,UUID v2的规范也改为由DCE1.1提供。
历史的车轮继续向前。久而久之,在应用范围内,UUID v1和v2的一些问题也随之暴露,包括但不限于:
MAC地址带来的隐私问题
安全问题
自定义MAC地址带来的碰撞概率增加
于是,UUID v3和v5也应运而生。
他们相对于v1和v2,最大改动是node从由基于MAC地址转换为基于命名空间名称。
命名空间名称其实本身就是一个UUID,RFC 4122规范定义了UUID以表示其用途:
| 名称 | UUID(固定值) | 用途 |
|---|---|---|
NAMESPACE_DNS | 6ba7b810-9dad-11d1-80b4-00c04fd430c8 | 用于 DNS 域名 |
NAMESPACE_URL | 6ba7b811-9dad-11d1-80b4-00c04fd430c8 | 用于 URL |
NAMESPACE_OID | 6ba7b812-9dad-11d1-80b4-00c04fd430c8 | 用于 OID |
NAMESPACE_X500 | 6ba7b814-9dad-11d1-80b4-00c04fd430c8 | 用于 X.500 Distinguished Names |
将上述UUID转换为string后在后面加上输入内容,再使用MD5进行散列,生成128位,然后对固定值进行替换即可得到一个UUID v3。
UUID v5与UUID v3具有相似性,它们之间不同的是,UUID v5使用SHA1进行散列而非v3使用的MD5。由于SHA1会生成160位,因此在替换前会将其先行截断为128位再进行操作。
这个改动解决了v1和v2长久以来遗留下来的痛点,但是由于其还可能存在由相同输入和命名空间导致的较大碰撞概率,我们还需要对其进行改进。
那么——
掌声有请,目前使用范围最为广泛,随机性最强的UUID v4!
UUID v4除了固定值的所有位都将随机生成,确保不会有任何生成前置条件、相同输入和命名空间的碰撞概率、安全及隐私问题等。
——它就是一串近乎随机生成的HEX!
UUID v4存在两个变体,其中2到3位用于表示变体版本(10用于表示变体1,110用于表示变体2)。因此对于UUID v4的变体1,有足足122位可用于随机生成,即共有$5.3 \times 10^{36}$个UUID v4变体1;而变体2因可用随机位少1位,故其数量只有变体1的一半。
这么多种UUID,很难不忽略其为人类计算机发展做出的贡献。那么UUID的问题至此已经完全解决了
吗?
UUID v6/v7/v8——战未来
的确,UUID v1至v5提供了多种选择和标识,但是其历史包袱还没完全放下。
2024年,随着RFC 9562的Published,UUID v6/v7/v8也随之推出。
UUID v6作为v1的重组版本,着重解决了v1的私密性问题,并针对数据库管理中的时间顺序排序进行了优化。详细结构改进如下:
| 字段 | 大小 | 描述 |
|---|---|---|
timestamp_hi | 32 | 时间戳高位 |
version | 4 | 固定为6 |
timestamp_mid | 16 | 时间戳中间位 |
timestamp_low | 12 | 时间戳低位 |
variant | 2 | 标识为RFC标准的UUID |
sequence | 14 | 用于避免重复 |
node | 48 | 通常为设备的MAC地址 |
而UUID v7由于其对于web应用、数据库等场景的极强友好性,成为了目前最具发展潜力的版本。
UUID v7的时间戳由使用了40年的格里高利历换为Unix时间,便于数据库管理和排序;中位和低位也改为随机生成,保持了唯一性。其结构如下:
| 字段 | 大小 | 描述 |
|---|---|---|
unix_ts_ms | 48 | Unix 时间戳(毫秒) |
version | 4 | 固定为7 |
rand_a | 12 | 随机字段A |
variant | 2 | 标识为RFC标准的UUID |
rand_b | 62 | 随机字段B |
而UUID v8有极强的战未来性质。它允许开发者完全自定义122位UUID内容,仅保留6位用于辨识版本和变体。由于其可自定义性过强,在此按下不表。
至此,UUID版本发展史全部结束。
UUID v7的Java实现例
Java提供了UUID标准库java.util.UUID,使用randomUUID()即可生成一个UUID v4:
UUID uuid = UUID.randomUUID();
但由于Java标准库还未跟进RFC 9562,所有我们无法直接使用其创建UUID v7。
我们也可以使用第三方库,比如uuid-creator,来创建UUID v7。
<dependency>
<groupId>com.github.f4b6a3</groupId>
<artifactId>uuid-creator</artifactId>
<version>6.1.1</version>
</dependency>
UUID uuid = UuidCreator.getTimeOrdered();
但是这看起来依旧不是很优雅,不是吗?
不过没关系,我们可以自己动手丰衣足食。
前文中我们已经了解UUID v7的基本结构,所有我们完全可以自己创建一个UUID v7。
下面的生成过程参考自David Ankin:
先初始化16字节的随机内容:
SecureRandom random = new SecureRandom();
byte[] value = new byte[16];
random.nextBytes(value);
并插入时间戳:
ByteBuffer timestamp = ByteBuffer.allocate(Long.BYTES);
timestamp.putLong(System.currentTimeMillis()); // 64位时间戳
System.arraycopy(timestamp.array(), 2, value, 0, 6); // 取低48位,即后6字节,并覆盖
随后我们设置其版本和变体:
value[6] = (byte) ((value[6] & 0x0F) | 0x70); // 保留低4位,并将高四位设为0111,即表示版本为7
value[8] = (byte) ((value[8] & 0x3F) | 0x80); // 保留低6位,并将高四位设为10,即表示变体
最后我们便可利用标准库来构造UUID对象了:
ByteBuffer buf = ByteBuffer.wrap(value);
long high = buf.getLong(); // 取前8字节作为高位
long low = buf.getLong(); // 取后8字节作为低位
return new UUID(high, low); // 构造UUID对象
是不是很简单?
下面是完整方法,希望能帮助到看到这里的每一个人:
public static UUID generateUUIDv7() {
SecureRandom random = new SecureRandom();
byte[] value = new byte[16];
random.nextBytes(value);
ByteBuffer timestamp = ByteBuffer.allocate(Long.BYTES);
timestamp.putLong(System.currentTimeMillis());
System.arraycopy(timestamp.array(), 2, value, 0, 6);
value[6] = (byte) ((value[6] & 0x0F) | 0x70);
value[8] = (byte) ((value[8] & 0x3F) | 0x80);
ByteBuffer buf = ByteBuffer.wrap(value);
long high = buf.getLong();
long low = buf.getLong();
return new UUID(high, low);
}
超越UUID
我们也可以不把目光局限在UUID身上,除此之外,我们还有很多选择:
ULID
ULID与UUID v7的设计理念非常接近,它将基于时间戳的排序与随机性相结合,确保其单调性。
Snowflake
Snowflake由Twitter提出,因其64位长整数形式的ID而闻名。但其与UUID不兼容,特别是在数据库迁移等时候需要十分注意。
KSUID
KSUID由Segment开发,旨在解决Snowflake的一些局限性,它对于UUID使用者和Snowflake使用者都十分友好。
无论是UUID的哪个版本,还是ULID、Snowflake、KSUID等替代品,他们的设计背后都有其特定的应用场景和技术权衡。
UUID以其标准化和广泛兼容性成为了事实上的唯一标识符基础设施,但在需要时间排序、分布式生成或更高可读性的现代系统中,新的方案正逐步补位甚至替代。
选择哪种方案,并不是单纯追求更新或更快,而应考虑实际需求:是否需要排序?是否运行在高并发环境中?是否关注数据泄漏风险?理解UUID的过去、现在与未来,是构建健壮系统的第一步。
唯一性,从未如此多样。
全文完。
参考内容及感谢
P. Leach, Microsoft, M. Mealling, Refactored Networks, LLC, R. Salz, DataPower Technology, Inc.. A Universally Unique IDentifier (UUID) URN Namespace. 2005.
K. Davis, Cisco Systems, B. Peabody, Uncloud, P. Leach, University of Washington. Universally Unique IDentifiers (UUIDs). 2024.
俞凡. 从 UUID 到 UUIDv7:唯一标识符的演进. 2025.
Rick Branson. A brief history of the UUID. 2017.
JackieDYH. UUID-五个版本-v1|v2|v3|v4|v5-使用说明. 2022.
yibin. 解析Twitter的雪花算法及其变体. 2023.
感谢来自David Ankin的部分Java实现思路。
感谢ChatGPT与Gemini提供部分非关键文案。