UUID vs GUID
2026-06-12 19:55:33 阿炯

本站赞助商链接,请多关照。 UUID

通用唯一识别码(英语:Universally Unique Identifier,缩写:UUID)是用于计算机体系中以识别信息的一个128位标识符。其按照标准方法生成时,在实际应用中具有唯一性,且不依赖中央机构的注册和分配。UUID重复的概率接近零,可以忽略不计。可以在无需任何中心协调的情况下独立生成,这使其成为分布式系统、数据库和 API 的理想选择。

因此所有人都可以自行建立和使用UUID,而且几乎可以确定其不会与既有的标识符重复。也因为如此,在不同地方产生的UUID可以使用于同一个数据库或同一个频道中,而且几乎不可能重复。其应用相当普遍,许多计算平台都提供了对于生成和解析UUID的支持。

1990年代,UUID 原本是用于阿波罗电脑的网络计算系统,后被用于开放软件基金会的分布式运算环境。分布式运算环境UUID的初始设计基于网络计算系统UUID,其设计受 Domain/OS 中定义和使用的(64位)唯一标识符的启发,这是一个也由阿波罗电脑设计的操作系统。后来,Microsoft Windows平台采用分布式运算环境设计作为全局唯一标识符(GUID)。2005年7月,RFC 4122 为 UUID 注册了一个 URN 命名空间,并制定了早期的规范。当 RFC 4122 作为互联网工程任务组标准发布时,国际电信联盟基于先前的标准和 RFC 4122 早期版本标准化了 UUID。

由开放软件基金会标准化,作为分布式运算环境(DCE)的一部分。UUID 被纪录为 ISO/IEC 11578:1996 "Information technology – Open Systems Interconnection – Remote Procedure Call(RPC)" 和后来的 ITU-T Rec. X.667 | ISO / IEC 9834-8:2005 规范的一部分。

互联网工程任务组公布了标准 RFC 4122,技术上等同于 ITU-T Rec. X.667 | ISO/IEC 9834-8。

格式

在其规范的文本表示中,UUID 的 16 个 8 位字节表示为 32 个十六进制数字,由连字符 '-' 分隔成五组显示,形式为“8-4-4-4-12”总共 36 个字符(32 个十六进制数字和 4 个连字符)。因此每一位十六进制数对应 4 个比特(bit);一个完整的 UUID 有 32 位十六进制数,总计 128 比特。例如:
123e4567-e89b-12d3-a456-426655440000
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx

UUID 由 RFC 9562(前身 RFC 4122)标准化,其标准字符串表示形式为 32 个十六进制数字,分为 5 组,并用连字符分隔:xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx

其中:M 表示 UUID 的版本(Version)N 表示 UUID 的变体(Variant)。

四位数字 M表示 UUID 版本,数字 N的一至三个最高有效位表示 UUID 变体。在例子中,M 是 1 而且 N 是 a(10xx2),这意味着此 UUID 是“变体1”、“版本1”UUID;即基于时间的 DCE/RFC 4122 UUID。

规范的 `8-4-4-4-12` 格式字符串基于 UUID 的16个字节的“记录布局”,因此其记录结构如下:
名称 长度 (字节) 长度(16进制数字码长) 说明
time_low 4 8 整数:低位 32 bits 时间
time_mid 2 4 整数:中间位 16 bits 时间
time_hi_and_version 2 4 最高有效位中的 4 bits“版本”,后面是高 12 bits 的时间
clock_seq_hi_and_res clock_seq_low 2 4 最高有效位为 1-3 bits“变体”,后跟13-15 bits 时钟序列
node 6 12 48 bits 节点 ID

这些字段对应于“版本1”和“版本2”(基于时间的)UUID中的字段,但是“8-4-4-4-12”的表示适用于所有UUID,即使对于生成方式不同的UUID也是如此。

RFC 4122 第 3 节要求以小写形式生成字符,同时对输入不区分大小写,尽管一些常用的实现违反了此规则。

Microsoft GUID有时会以大括号表示:
{123e4567-e89b-12d3-a456-426655440000}

不应将此格式与“Windows注册表格式”相混淆,后者指的是大括号内的格式。

RFC 4122为UUID定义了统一资源名称(URN)命名空间。作为URN呈现的UUID如下:
urn:uuid:123e4567-e89b-12d3-a456-426655440000

变体

UUID的变体(variant)字段,占1到3比特。RFC 4122定义了4种变体:
1).变体 0 (最显著位为0,二进制为0xxx2,十六进制表示为016到716) 用于向后兼容已经过时的1988年开发的 Apollo 网络计算系统(NCS)1.5 UUID 格式。 前6字节是48比特时间戳(从1980年1月1日UTC开始的4微秒的滴答数),随后2个字节保留,再1个字节是地址族(address family,使用了0..13个情形),最后7个字节是主机ID。这类似于UUID的版本1.

2).变体 1 (二进制为10xx2,十六进制为816到b16),定义在RFC 4122/DCE 1.1 UUIDs, 或"Leach–Salz" UUID。它是按照大端序作为二进制存储与传输。

3).变体 2 (二进制为110x2,十六进制为c16或d16),RFC称“保留,微软公司向后兼容”。早期的Microsoft Windows平台的GUID使用该格式。和完全使用大端序的变体 1 不同,变体 2 其中一部分按照小端序作为二进制存储与传输。

4).形如111x2保留未使用。

目前的UUID规范使用变体1和2。在文字表示上,两种变体只有代表变体的比特不同。上面的字段也定义了两种变体之间的字节转换。前三个字段是无正负号的32和16位数字,需要进行转换,而后两个字段是由未解释的字节组成,不需要进行转换。这种转换同样适用于版本3、4和5,其中的规范字段与UUID的内容无关。

虽然一些重要的GUID名义上是变体2的UUID,例如组件对象模型(Component Object Model)IUnknown界面的标识符,但是在微软Windows软件中所生成和使用的、被称为“GUID”的许多标识符是使用标准的变体1的RFC 4122/DCE 1.1大端序UUID。目前 Microsoft guidgen工具软件产生变体1的结果。某些微软文档称GUID与UUID是同义词,如同RFC 4122中表示UUID“也被称作GUID”。这些文件表明了虽然“GUID”最初指代微软所使用的其中一种UUID变体,但现在已经成为UUID的另一个名称,含括变体1和2。

对于“变体(variants)1”和“变体2”,标准中定义了五个版本(versions),并且在特定用例中某些版本可能比其他版本更合适。版本由字符串中的 M 指示。

版本1的UUID是根据时间和节点ID(通常是MAC地址)生成;版本2的UUID是根据标识符(通常是组或用户ID)、时间和节点ID生成;版本3、版本5透过对命名空间(namespace)标识符和名称进行散列生成确定性的UUID;版本4的UUID则使用随机性或伪随机性生成。

Nil UUID
这是一个特例,值为 00000000-0000-0000-0000-000000000000 ;也就是说所有位都设置为 0。它被保证不是唯一的。

通常用作 UUID 格式的模板,或者在你需要一个易于识别且明确不唯一的 UUID 时使用。

Max UUID(最大 UUID)
这是一种特殊的 UUID,其定义为 128 个比特位全部置为 1。可以将其视为Nil UUID(全零 UUID)的“反向”形式:
FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF

在某些场景下,当系统需要一个 128 位的 UUID,但又想表达诸如 “UUID 列表结束” 这样的概念时,就可以将 Max UUID 用作一个哨兵值(sentinel value)。它被预留用于此类特定于实现的需求。

版本1(日期时间和MAC地址)
版本1的UUID,是根据 60-bit 的时间戳和节点(生成UUID的计算机)的48-bit MAC地址而生成的。


将时间戳、时钟序列(Clock Sequence)和节点 ID(通常是 MAC 地址)组合而成。当你需要知道一个 UUID 的创建时间时,它非常有用。

版本2(日期时间和MAC地址,DCE安全版本)
RFC 4122 保留了版本2的UUID用于“DCE security”;但并没有提供任何细节。因此许多 UUID 实现省略了“版本2”。但“版本2”UUID 的规范由 DCE 1.1 身份验证和安全服务规范提供。


版本3和版本5(基于命名空间名称)
“版本3”和“版本5”的 UUID 透过散列(hashing)命名空间标识符和名称生成。版本3使用 MD5 作为散列算法,版本5则使用 SHA1。


名称空间标识符本身就是一个 UUID。该规范提供了 UUID 用来表示命名空间为了统一资源定位符(URLs),完整域名、对象标识符和 X.500;但任何所需的UUID都可以用作命名空间指示符。

它俩有一个特性:相同名称空间和名称将映射到同一个UUID;然而,即使已知其中一项,也无法透过暴力搜索之外的方法从UUID逆向推导出另外一项。RFC 4122 推荐使用版本5(SHA1)而不是版本3(MD5),并建议不要使用任一版本的 UUID 作为安全凭证。

规范预定义了 4 个标准命名空间:
DNS: 6ba7b810-9dad-11d1-80b4-00c04fd430c8
URL: 6ba7b811-9dad-11d1-80b4-00c04fd430c8
OID: 6ba7b812-9dad-11d1-80b4-00c04fd430c8
X.500 DN: 6ba7b814-9dad-11d1-80b4-00c04fd430c8

当在 v3 或 v5 UUID 生成器中选择一个预定义命名空间时,系统会自动将其转换为对应的 UUID。



最佳用途:需要 MD5 型 UUID 的遗留系统、向后兼容。
注意:使用 MD5 算法(虽然不适用于加密安全场景,但对于 UUID 生成本身是足够的)。

版本4(随机)
版本4的UUID是随机生成的。与其他 UUID 一样,其中4位用于代表“版本4”,2到3比特代表变体号(102 或 1102 分别用于变体 1 和 2)。因此对于变体1(即大多数 UUID),随机生成的版本4的UUID会保留6比特用于表示变体号和版本号,其余122比特用于随机生成,版本4变体2的UUID(传统GUID)则为变体1的一半,因为可用的随机位少一个,变量消耗 3 比特。



在 Microsoft 系统上,v4, variant-2 被称为 “GUID”。GUID 是微软对 DCE UUID 的一种实现,基本遵循 RFC 4122 标准,唯一的区别在于字节序(endianness)。

最佳用途:绝大多数应用、数据库、API、会话令牌(Session Token)。
唯一性保障:2^122 个可能的取值。

版本6(重排序的时间戳型)
v6 是 v1 的一个重新排序版本,它调整了时间戳部分的比特位顺序,使其具备天然的可排序性。它使用与 v1 相同的 60 位格里高利历时间戳,但将最高有效位放在前面。



最佳用途:当需要可排序性时,作为 v1 的直接替代方案。
优势:在保持与 v1 兼容的同时,实现了字典序(Lexicographical)排序。

版本7(Unix 时间戳,可排序型)
这是 RFC 9562 中定义的最新版本。它将 Unix 时间戳(以毫秒为单位)编码到 UUID 中,使得 v7 UUID 能够按创建时间自然排序。



最佳用途:数据库主键、按时间排序的事件、现代分布式系统。
优势:兼具可排序性和随机性,相比 v1 更适合现代数据库的需求。

版本8
UUIDv8 为实验性或厂商特定的使用场景提供了一种格式。其唯一强制要求是:变体(variant)必须按照RFC规定进行设置。



其唯一性完全取决于具体实现,使用者绝不能对其唯一性做出任何假设。以下是一些可能使用 UUIDv8 的典型场景:
某个实现希望在 UUID 中嵌入本文档未定义的额外信息。
某个实现受到特定应用程序或编程语言的限制,无法使用现有的任何一种标准 UUID。

小结一下变体(Variant)与版本(Version)

版本(Version)
十六进制数字 (M) UUID 版本
0 Nil UUID
1 version-1 基于格里高利历时间戳的 UUID
2 version-2 保留,用于 DCE 安全版本(内嵌 POSIX UUID)
3 version-3 基于名称的 UUID,使用 MD5 哈希算法
4 version-4 随机或伪随机生成的 UUID
5 version-5 基于名称的 UUID,使用 SHA-1 哈希算法
6 version-6 重排序版基于格里高利历时间戳的 UUID
7 version-7 基于 Unix 时间戳(毫秒)的 UUID
8 version-8 用于本文档中定义的自定义 UUID 格式
9 - e 未知版本
f Max UUID

UUID 的变体(Variant)
要确定变体,需要分析 UUID 第 17 位(“N”)所对应的 4 个二进制位。
关键在于这 4 位二进制数的开头模式:
如果以 10 开头,则变体为 “DCE 1.1, ISO/IEC 11578:1996”(这是最常见的标准变体);
如果以 110 开头,则该 UUID 是一个 “Microsoft GUID”。

下表给出了完整的变体对照关系:
二进制表示 十六进制数字 (N) 变体 (Variant)
0xxx 0 - 7 保留(用于兼容旧的 NCS 标准)
10xx 8 - b DCE 1.1, ISO/IEC 11578:1996(标准 UUID)
110x c - d 保留(Microsoft GUID)
1110 e 保留(供将来使用)
1111 f 未知 / 无效(必须以 0 结尾)

常见的 UUID 格式
格式 示例 用途
标准格式 550e8400-e29b-41d4-a716-446655440000 最常用
花括号格式 {550e8400-e29b-41d4-a716-446655440000} Windows 注册表
URN 格式 urn:uuid:550e8400-e29b-41d4-a716-446655440000 符合 RFC 4122 的统一资源名称(URN)
无连字符格式 550e8400e29b41d4a716446655440000 紧凑存储(节省空间)

应用

1、文件系统
重要用途包括 ext2/ext3/ext4 文件系统用户空间工具(e2fsprogs 使用 util-linux 提供的 libuuid)、LUKS 加密分区、GNOME、KDE 和 Mac OS X,其中大部分源自 曹子德(Theodore Ts'o)的实现。

分区标签和分区UUID都存储于超区块中。两者皆为文件系统的一部分,而不是分区的一部分。例如,ext2~4包含UUID,而NTFS或FAT32则没有。

超区块是文件系统的一部分,因此被完全包含在分区中,因此如果你执行dd if=/dev/sda1 of=/dev/sdb1,sda1和sdb1都会拥有相同的标签和UUID。

2、COM
Microsoft的组件对象模型(COM)中使用了几种GUID :
IID - 接口标识符;(在系统上注册的接口标识符存储在Windows注册表中的[HKEY_CLASSES_ROOT\Interface])
CLSID - 类标识符;(存储在[HKEY_CLASSES_ROOT\CLSID])
LIBID - 类型库标识符;(存储于[HKEY_CLASSES_ROOT\TypeLib])
CATID - 类别标识符;(它在一个类中的存在将其识别为属于某些类别类别,列于[HKEY_CLASSES_ROOT\Component Categories])

3、作为数据库主键
UUID 通常用作数据库表中的唯一键。下文还要开一节继续介绍。

Microsoft SQL Server 版本4 Transact-SQL 中的 NEWID 函数会返回标准随机版本4的UUID,而 NEWSEQUENTIALID 函数返回类似于 UUID 的 128 位标识符,这些 UUID 会依序递增,直到下次系统重启。另一方面,尽管名称如此,但 Oracle Database SYS_GUID 函数不会返回标准 GUID;相反它根据主机标识符和进程或线程标识符返回一个16字节的 128 位 RAW 值,有点类似于 GUID。

PostgreSQL 包含一个 UUID 数据类型,并且可以通过使用模块中的函数生成大多数版本的UUID。

MySQL 提供了一个 UUID 函数,它生成标准的版本1 UUID。

当 UUID 用作主键时,版本3、4和5 UUID 的随机性以及 版本1和2 UUID 内的字段的排序可能会产生数据库定位或性能问题。例如,2002年 Jimmy Nilsson 报告说,当用作主键的版本4 UUID 被修改为包含基于系统时间的非随机后缀时,Microsoft SQL Server的性能显着提高。Nilsson 承认,这种所谓的“COMB”(组合时间和GUID)方法使UUID非标准并且更有可能被复制,但 Nilsson 仅要求在应用程序中的唯一性。透过重新排序和编码版本1和版本2的UUID,将时间戳放在最前面,可以避免插入所造成的性能损失。

诸如Laravel这样的部分网络框架支持“时间戳优先”的UUID,可以将UUID有效存储于索引数据库中。这种UUID是版本4格式的COMB UUID,但其中前48位组成了一个时间戳,就像版本1的UUID一样。其他基于COMB UUID概念的指定格式包括:
1).ULID,其抛弃了用于表示版本4的4位,并且默认使用base32编码。
2).UUID版本6到8是三种COMB UUID格式的正式提议。


GUID

全局唯一标识符(英语:Globally Unique Identifier,缩写:GUID;发音为/ˈɡuːɪd/或/ˈɡwɪd/)是一种由算法生成的唯一标识,通常表示成32个16进制数字(0-9,A-F)组成的字符串,如:{21EC2020-3AEA-1069-A2DD-08002B30309D},它实质上是一个128位长的二进制整数。GUID一词有时也专指微软对UUID标准的实现。

GUID的主要目的是产生完全唯一的数字。在理想情况下,任何计算机和计算机集群都不会生成两个相同的GUID。GUID的总数也足够大,达到了2128(3.4×10^38)个,所以随机生成两个相同GUID的可能性是非常小的,但并不为0。所以用于生成GUID的算法通常都加入了非随机的参数(如时间),以保证这种重复的情况不会发生。

Windows操作系统使用GUID来标识COM对象中的类和界面。一个脚本可以不需知道DLL的位置和名字直接通过GUID来激活其中的类或对象。

英特尔的全局唯一标识分区表使用GUID来标识硬盘和分区。微软的ActiveX使用UUID来标识每一个不同的浏览器控件。

基本结构

GUID本质上是一个16字节(128位)的二进制数,最常见的结构如下:
字节 描述 字节序
32 4 数据1 原生
16 2 数据2 原生
16 2 数据3 原生
64 8 数据4 大端序

数据4的字节序和GUID显示成文本的结果相同,而其它3个数据在小端序的机器(如英特尔的CPU)上必须先转换成大端序。

数据4的第一个字节的第1-3位表示所使用的GUID变种类型:
模式 描述
0 向后兼容网络计算系统
10 标准
110 向后兼容微软组件对象模型
111 保留至将来使用

数据3最高的4位表示版本号和所使用的算法。

文本编码

GUID通常会写成16进制数的字符串,如:
3F2504E0-4F89-11D3-9A0C-0305E82C3301

这种文本表示包括了如下部分:
16进制数的位数 描述
8 数据1
4 数据2
4 数据3
4 数据4的最初2字节
12 数据4的剩余6字节

上述表示方法通常放在一对大括号里边,如:
{3F2504E0-4F89-11D3-9A0C-0305E82C3301}

当需要使用更少的字符表示GUID时,可能会使用Base64或Ascii85编码。Base64编码的GUID有22-24个字符,如:
7QDBkvCA1+B9K/U0vrQx1A
7QDBkvCA1+B9K/U0vrQx1A==

Ascii85编码后是20个字符,如:
5:$Hj:Pf\4RLB9%kU\Lj

在URN中,GUID第一版的命名空间标识是"uuid",如:
urn:uuid:3F2504E0-4F89-11D3-9A0C-0305E82C3301


两者的区别

如果在Windows生态圈特别是com生态圈经历过,肯定见过 GUID。如果接触过数据库、API 或 Unix 系统,也一定遇到过 UUID。它们看起来一模一样——都是带连字符的 32 位十六进制数。但总在争论该叫哪个名字,它们之间真的有区别吗?

简短回答:几乎没有。但就是这“几乎没有”的部分,在某些特定场景下却至关重要。今天,我们就来彻底厘清这个问题。
1).UUID(Universally Unique Identifier,通用唯一标识符)是 RFC 9562(前身是 RFC 4122)定义的国际标准。它是大多数编程语言、数据库和协议中广泛认可的术语。

2).GUID(Globally Unique Identifier,全局唯一标识符)是 微软对同一概念的称呼。它起源于 COM(组件对象模型)时代,并一直沿用至今,贯穿整个 Windows 和 .NET 生态系统。

它们采用相同的格式,都是 128 位的值,拥有完全一致的带连字符的十六进制字符串表示。在 Linux 上生成的 UUID,在 Windows 上就是一个有效的 GUID,反之亦然。为什么会有两个名字呢?

UUID 的起源
UUID 的概念最早由 Apollo Computer 公司在 20 世纪 80 年代提出,作为其网络计算系统(NCS)的一部分。后来,开放软件基金会(OSF)将其纳入分布式计算环境(DCE)规范。最终,这一规范在 2005 年被正式确立为 RFC 4122,并于 2024 年更新为 RFC 9562。

其目标非常明确:让任何计算机都能独立生成一个标识符,无需与中央机构协调,就能保证其在全球范围内的唯一性。

微软的 GUID
到了 90 年代,微软在开发 COM 和 DCOM 技术时,同样需要为组件接口、类和类型库生成唯一的标识符。他们没有直接采用UUID这个术语,而是将其命名为 GUID(全局唯一标识符)。

其实现基于 DCE UUID 规范,但也带有一些微软特有的行为:
1).Windows 提供了 CoCreateGuid() 和 UuidCreate() 等 API 函数。
2).Visual Studio 在项目文件、接口和 COM 注册中大量使用 GUID。RAD Studio使用了MS Build的构建生态,产生同样GUID依赖。
3)..NET Framework 将 System.Guid 作为一个核心内置类型。

这个品牌名称就此深入人心。几十年过去了,.NET 开发者们依然习惯说 “GUID”,而业界其他地方则普遍使用 “UUID”。

技术差异

字节序(Endianness)
最实质性的技术差异在于字节序——即多字节数据在内存中的存储方式。

UUID (RFC 标准):前三个字段采用大端序(网络字节序)。
time_low: 4 字节,大端序
time_mid: 2 字节,大端序
time_hi_and_version: 2 字节,大端序
clock_seq: 2 字节,大端序
node: 6 字节

Microsoft GUID:采用混合字节序——前三个字段是小端序(x86 原生),后两个字段则是大端序。
Data1: 4 字节,小端序
Data2: 2 字节,小端序
Data3: 2 字节,小端序
Data4: 8 字节,大端序

在实践中意味着什么
当看到字符串形式(如 550e8400-e29b-41d4-a716-446655440000)时,两者是完全一样的。差异只存在于二进制表示中——也就是字节在内存或磁盘上的实际排列顺序。

以 UUID 00112233-4455-6677-8899-aabbccddeeff 为例:
格式 内存中的字节序列
RFC UUID (大端序) 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff
Microsoft GUID (混合端序) 33 22 11 00 55 44 77 66 88 99 aa bb cc dd ee ff

可以看到,前 8 个字节被分组并进行了反转。这种情况可能会在以下场景引发 Bug:
1).在 SQL Server(存储的是 GUID 的二进制)和 Web API(通常处理 UUID 字符串)之间进行二进制转换。
2).在非 Windows 工具中读取 Windows 注册表里的 GUID 二进制数据。
3).在 COM 组件与非微软系统之间进行互操作。

字符串格式完全一致
当序列化为字符串时,GUID 和 UUID 是可以互换的:
550e8400-e29b-41d4-a716-446655440000

两者都使用 32 个十六进制数字,并用 4 个连字符分成 5 组(8-4-4-4-12)。大小写在技术上是不敏感的,但约定俗成有所不同:
UUID 通常使用小写:
550e8400-e29b-41d4-a716-446655440000

GUID在微软语境下常用大写,有时还会加上花括号:
{550E8400-E29B-41D4-A716-446655440000}

在哪里会碰到它们

UUID 领地
上下文 示例
PostgreSQL UUID 数据类型
Python uuid.uuid4()
Java java.util.UUID
JavaScript crypto.randomUUID()
RFC 规范 RFC 9562
Linux/macOS 工具 uuidgen 命令
REST APIs 文档中通常称为 "UUID"

GUID 领地
上下文 示例
.NET / C# System.Guid.NewGuid()
SQL Server UNIQUEIDENTIFIER 类型, NEWID()
Visual Studio .sln 项目文件中的项目 GUID
COM / DCOM 接口 ID、类 ID
Windows 注册表 组件注册信息
PowerShell [guid]::NewGuid()

某些场景下这两个术语会被互换使用:
MySQL 文档在解释时会同时提到 “UUID”(函数名)和 “GUID”。
Stack Overflow 上的问题经常将二者视为同义词。
API 文档的用词取决于作者的技术背景。


用作数据库主键的实践

数十年来,自增整数(Auto-increment Integer)一直是关系型数据库的默认主键选择。它简单、高效且被广泛支持。那么,为何越来越多的团队开始转向 UUID 呢?

答案在于现代应用的分布式本质。微服务需要独立生成 ID,移动客户端需要在离线状态下创建记录,数据需要在分片、副本和不同区域之间进行合并。而自增整数依赖一个中心化的权威节点来分配下一个数字——在分布式世界里,这无疑是一个性能瓶颈。

UUID 正是为解决此问题而生,但它也带来了新的权衡。如何规避其性能陷阱,探寻UUID作为数据库主键的最佳方案。

为何要将 UUID 用作主键

1. 分布式生成
任何服务、客户端或设备都可以独立生成 UUID,无需与中心数据库协调。你甚至可以在向数据库插入记录之前就知晓其 ID。

这使得以下场景成为可能:
离线优先(Offline-first)的应用,在设备上创建记录后异步同步。
微服务架构,各服务可自行生成 ID,无需调用数据库。
事件溯源(Event Sourcing),事件在持久化前即可携带自身 ID。
跨系统数据迁移,彻底避免 ID 冲突。

2. 避免信息泄露
自增 ID 会暴露敏感信息:
/api/users/15847 向攻击者暗示系统大约有 15,847 名用户。
顺序 ID 是可枚举的——攻击者可以轻易地遍历 /api/orders/1 到 /api/orders/99999。

而 UUID(如 7c9e6679-7425-40de-944b-e07fc1f90ae7)则完全不透露记录数量、顺序或创建时间(特指 v4 版本)。

3. 便于数据合并
当需要合并来自多个数据库的数据(如分库分表、多租户、公司并购)时,自增 ID 极易发生冲突。而 UUID 则天然无冲突。两个独立的数据库可以各自生成数百万条记录,合并时却毫无压力。

4. API 稳定性
UUID 是绝佳的对外公开标识符。它们不透明、非顺序,且不会因数据库结构调整而改变。最佳实践是:在 API 中暴露 UUID,而将自增整数作为内部实现细节(如果仍需使用的话)。

性能陷阱:随机 UUID 的代价
这里才是关键所在。在基于 B-tree 索引的数据库中,将随机 UUID(v4)用作主键会导致可测量的性能下降。原因如下:

B-Tree 索引的工作原理
大多数数据库(如 PostgreSQL、MySQL InnoDB)将主键索引存储为 B-tree。数据在物理上按主键顺序排列,当插入新行时:
顺序键(自增)新行总是追加到末尾。B-tree的最后一页会常驻内存(“热页”),插入操作快速且可预测。

随机键(UUID v4) 新行会随机插入到树的各个位置,导致:
页分裂(Page splits):满页需要分裂以容纳新数据。
缓存抖动(Cache thrashing):整个索引都需加载到内存,而非仅尾部。
碎片化(Fragmentation):页面利用率低,浪费存储空间。
写入放大(Write amplification):每次插入产生更多磁盘 I/O。

真实世界的影响
对于一张拥有千万级(10M+)数据的表:
使用 UUID v4 作为主键,插入速度可能比自增 ID 慢 2-5 倍。
由于碎片化,索引体积会增大 20%-40%。
范围扫描和分页查询变慢,因为逻辑上相邻的行在物理上是分散的。

终极解决方案:UUID v7
UUID v7 完美解决了上述性能问题。它在 ID 前缀嵌入了时间戳,使得生成的 UUID 具有时间序。新生成的 UUID 总是大于之前的,因此插入操作总是发生在 B-tree 的末尾——与自增整数的表现如出一辙。

实战指南:各数据库实现
PostgreSQL
-- 使用原生 UUID 类型(16字节,高效存储)
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- v4
    email VARCHAR(255) UNIQUE NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMPTZ DEFAULT now()
);

-- 对于 UUID v7 (PostgreSQL 17+)
CREATE TABLE orders (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    user_id UUID REFERENCES users(id),
    total DECIMAL(10,2)
);

提示:PostgreSQL 的 UUID 类型在内部以 16 字节存储,远比 36 字符的字符串表示更紧凑。

MySQL (InnoDB)
-- 为获得最佳性能,请存储为 BINARY(16)
CREATE TABLE users (
    id BINARY(16) PRIMARY KEY,
    -- 虚拟列用于方便地显示为标准格式
    id_text VARCHAR(36) GENERATED ALWAYS AS (
        INSERT(INSERT(INSERT(INSERT(
            HEX(id), 9, 0, '-'), 14, 0, '-'), 19, 0, '-'), 24, 0, '-')
    ) VIRTUAL,
    email VARCHAR(255) UNIQUE,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- MySQL 8.0+ 的 swap 标志可重排时间戳位,提升局部性
INSERT INTO users (id) VALUES (UUID_TO_BIN(UUID(), 1));

重要:在 InnoDB 中,主键就是聚簇索引。使用随机 UUID v4 作为主键会直接导致整个数据表碎片化,而不仅仅是索引。

SQLite
SQLite 没有原生 UUID 类型,可存储为 TEXT 或 BLOB。

CREATE TABLE items (
    id TEXT PRIMARY KEY, -- '550e8400-e29b-41d4-...'
    name TEXT
);

应用层 vs 数据库生成

在分布式系统中,强烈建议在应用层生成 UUID,而非依赖数据库:
// Node.js
import { v7 as uuidv7 } from 'uuid';
const newUser = { id: uuidv7(), email: 'user@example.com' };
await db.insert('users', newUser); // 插入前已知 ID

# Python
from uuid6 import uuid7
user = User(id=str(uuid7()), email="user@example.com")
session.add(user)

存储优化与混合模式
存储开销对比
存储格式 单个 ID 大小 百万行总大小
INT (自增) 4 字节 3.8 MB
BIGINT 8 字节 7.6 MB
UUID (二进制) 16 字节 15.3 MB
UUID (CHAR 36) 36 字节 34.3 MB

优化策略:
1).务必以二进制形式存储(PostgreSQL的UUID类型,MySQL的 BINARY(16)),切勿使用 CHAR(36)。

2).混合模式(Hybrid Pattern):内部使用自增整数保证性能,外部使用 UUID 保证分布式友好性。

-- 混合模式示例
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,    -- 内部主键,快速且紧凑
    public_id UUID UNIQUE DEFAULT uuidv7(), -- 对外暴露的 ID
    email VARCHAR(255) UNIQUE
);

-- 外键关联使用内部整数ID
CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT REFERENCES users(id),  -- 仅8字节,非16字节
    public_id UUID UNIQUE DEFAULT uuidv7()
);

常见误区规避指南
1.将 UUID 存为 VARCHAR(36):这会让所有比较、连接和索引操作处理 36 个字符,而非 16 字节。
2.盲目使用 UUID v4 而忽视性能成本:在小数据量下差异不明显,但在高并发、大数据量场景下,性能差距巨大。
3.在分布式系统中由数据库生成 UUID:这违背了分布式 ID 生成的初衷。
4.忽略对 UUID 外键列的索引:外键列不会自动被索引,务必手动创建。
5.以字符串形式比较 UUID:确保使用数据库的原生 UUID 类型进行比较,以获得最佳性能。

数据库使用UUID的常见问题解答 (FAQ)
Q:新项目该选 UUID 还是自增 ID?

A:如果是单体应用且只有一个数据库,自增 ID 更简单高效。如果是微服务、需要离线生成 ID 或希望对外提供不透明标识符,则选用 UUID v7。混合模式(内部整数 + 外部 UUID)能兼顾两者优势。

Q:UUID 主键比整数慢多少?
A:在高写入负载(每秒万次以上插入)和千万级数据量下,UUID v4 的插入速度可能慢 2-5 倍。而 UUID v7 的差距已缩小至 1.1-1.3 倍,对绝大多数应用而言可忽略不计。对于读多写少的场景,差异微乎其微。

Q:主流 ORM(如 Prisma, Django)支持 UUID 吗?
A:完全支持。Django 可用 models.UUIDField(primary_key=True),Prisma 可用 @default(uuid())。对于 UUID v7,建议在应用代码中生成,而非依赖 ORM 默认行为。

Q:在 URL 中暴露 UUID 安全吗?
A:UUID v4 是安全的,它不泄露任何信息。UUID v7 会暴露毫秒级的创建时间,需根据业务场景评估风险。两者均不会泄露记录总数或允许枚举。对安全性要求极高的场景,可考虑在暴露前对 UUID 进行加密或哈希处理。

Q:如何从自增 ID 迁移到 UUID 主键?
A:这是一个重大操作。基本步骤是:添加 UUID 列 -> 为现有数据填充 UUID -> 更新外键引用 -> 切换主键。对于大表,务必使用在线 DDL 工具(如 MySQL 的 pt-online-schema-change 或 PostgreSQL 的 pg_repack)以避免长时间锁表。

常见问题解答 (FAQ)

Q: GUID 和 UUID 是完全一样的东西吗?

A: 功能上,是的。两者都是 128 位标识符,拥有相同的字符串格式。唯一的技术差异在于二进制表示的字节序:微软用混合端序,RFC 标准用大端序。在字符串形式下,它们完全相同且可互换。

Q: 能在 PostgreSQL 里把 C# 生成的 GUID 当作 UUID 使用吗?

A: 完全可以。只要以字符串形式(如 "550e8400-e29b-41d4-a716-446655440000")传递,.NET 的 Guid 在 PostgreSQL 中就能完美作为 UUID 使用,数据库驱动会自动处理。只要避免在手动进行二进制转换时不考虑字节序差异即可。

Q: 为什么微软不直接用 UUID,而要另起炉灶叫 GUID?

A:  微软开发 GUID 的时间点与 UUID 规范的正式标准化几乎是同步的。COM  架构迫切需要一套自己的唯一标识方案,于是微软采用了自家的品牌命名。等到 RFC 4122 正式确立 “UUID” 这个术语时,“GUID”  早已深深植根于整个 Windows 生态系统,难以更改了。

Q: 字节序的差异真的会导致 Bug 吗?

A: 会,但仅限于特定场景:比如直接对 SQL Server 的 GUID 二进制数据和 PostgreSQL 的 UUID 二进制数据进行比较,或者在跨平台工具中直接读取 Windows 注册表的二进制 GUID。在字符串层面,没有任何区别,也绝不会出错。如今大多数应用都使用字符串表示,因此这个问题在实践中很少遇到。

Q: API 文档里该用哪个术语?

A: 用 UUID。它是 RFC 标准术语,被所有平台和语言所公认。即使你的团队主要使用 .NET,对外的 API 文档也应该采用这种通用术语。你的 .NET 开发者能理解 “UUID”,但并非所有人都知道 “GUID”。

跨平台陷阱
.NET 到 PostgreSQL
// C# 生成一个 GUID
var id = Guid.NewGuid();
// 当作为 UUID 参数传给 PostgreSQL 时,驱动会自动处理转换
// 但如果你手动转成字节数组,就要小心字节序!
byte[] bytes = id.ToByteArray(); // 这是混合端序!
// 要转换成 RFC 标准的大端序字节数组:
byte[] rfcBytes = new byte[16];
// 反转前4字节
rfcBytes[0] = bytes[3]; rfcBytes[1] = bytes[2];
rfcBytes[2] = bytes[1]; rfcBytes[3] = bytes[0];
// 反转第4-5字节
rfcBytes[4] = bytes[5]; rfcBytes[5] = bytes[4];
// 反转第6-7字节
rfcBytes[6] = bytes[7]; rfcBytes[7] = bytes[6];
// 复制剩余字节
Array.Copy(bytes, 8, rfcBytes, 8, 8);

SQL Server 到 Web API

SQL Server 的 UNIQUEIDENTIFIER 以微软的混合端序存储二进制数据。但当通过 JSON API 暴露时,其字符串表示与 UUID 完全相同。对于字符串,无需任何转换;只有在进行二进制比较时才需注意。

Java 到 .NET

Java 的 java.util.UUID 和 .NET 的 System.Guid 在处理字符串格式时都能无缝工作。只有在直接比较二进制表示时才会出现问题。

其他唯一标识符格式
在编写严谨的技术文档时,UUID 是更准确、更通用的术语,因为它指向了实际的国际标准(RFC 9562)。而 GUID 则是一个厂商特定的名称。

在评估唯一标识符方案时,可以看看 GUID/UUID 与其他选项的对比:
格式 大小 可排序 标准 适用场景
UUID v4 128-bit RFC 9562 通用场景
UUID v7 128-bit RFC 9562 数据库主键
ULID 128-bit 社区规范 时间排序ID
Nanoid 可配置 社区规范 短链接、URL安全ID
Snowflake ID 64-bit Twitter/Discord 高吞吐分布式系统
KSUID 160-bit Segment K-可排序唯一ID
CUID2 可变 社区规范 安全、抗碰撞

每种方案都有其独特优势,而 UUID 凭借其最广泛的生态系统支持和标准化程度,依然是首选。

参考链接:

UUID-Cn
GUID-Cn
rfc-4122
rfc-9562