写点什么

深度解读 UUID:结构、原理以及生成机制

作者:EquatorCoco
  • 2024-03-21
    福建
  • 本文字数:8339 字

    阅读完需:约 27 分钟

What 是 UUID


UUID (Universally Unique IDentifier) 通用唯一识别码 ,也称为 GUID (Globally Unique IDentifier) 全球唯一标识符。

UUID 是一个长度为 128 位的标志符,能够在时间和空间上确保其唯一性。UUID 最初应用于 Apollo 网络计算系统,随后在 Open Software Foundation(OSF)的分布式计算环境(DCE)中得到应用。可让分布式系统可以不借助中心节点,就可以生成唯一标识, 比如唯一的 ID 进行日志记录。

并被微软 Windows 平台采用。Windows 举例 2 个使用场景:


  • COM 组件通过 GUID 来定义类标识符(CLSID)、接口标识符(IID)以及其他重要的标识,确保在整个系统中不会发生命名冲突。



  • Windows 注册表中很多项都使用 GUID 作为子键名,以便为特定程序或功能提供一个全球唯一的注册表路径。



UUID 之所以被广泛采用,主要原因之一是它们的分配不需要中心管理机构介入。其具有唯一性和持久性,它们非常适合用作统一资源名称(URN)。UUID 能够无需注册过程就能生成新的标识符的独特优点,使得 UUID 成为创建成本最低的 URN 类型之一。

那么 UUID 会重复嘛,由于 UUID 具有固定的大小并包含时间字段,在特定算法下,随着时间推移,理论上在大约公元 3400 年左右会出现值的循环,所以问题不大。

由于 UUID 是一个 128 位的长的标志符,为了便于阅读和显示,通常会将这个大整数转换成 32(不包含连接符)个十六进制字符组成的字符串形式。如下

crypto.randomUUID()// 4d93f326-3f48-4a43-929d-b6489f4754b5
`${crypto.randomUUID()}`.length // 长度:36
`${crypto.randomUUID()}`.replace(/-/g, '').length// 去掉连接符:32
复制代码


这 128 位的组成,以及是怎么变成 32 位的十六进制字符的,继续往下看:


UUID 的结构

UUID 看似杂乱无章,其实内有乾坤,慢慢道来。


必须了解的


  • 比特(bit):二进制数字系统中的基本单位。一个比特可以代表二进制中的一个 0 或 1。

  • 位(通常情况下与比特同义):二进制数系统中的一位,同样表示 0 或 1。

  • 字节(Byte):字节是计算机中更常用的单位,用于衡量数据存储容量和传输速率。1 字节等于 8 个比特。

总结起来就是:

  • 1 字节 = 8 位

  • 1 位 = 1 比特

128 位转为 32 个十六进制字符, 这个十六进制字符是什么呢,其专业名字为 hexDigit,是 UUID 中我们肉眼可见的最小单元。


hexDigit


hexDigit , 十六进制数字字符,是一个长度为 4 比特,可以表示 0(0b000)到 15(0b1111)之间数值。其能转为 16 进制的相对应符号,其取值范围为 0-9,a-f, A-F, 即0123456789abcdefABCDEF某一个值。

所以, hexDigit 可以粗暴的理解为 0123456789abcdefABCDEF某一个值

 (0b1000).toString(16)     // 8 (0b1111).toString(16)     // F
复制代码


此外,还有一个 hexOctet, 两个连续hexDigit组成的序列, 占 8 个比特,即一个字节。


UUID 基本结构


在协议 RFC 4122: A Universally Unique IDentifier (UUID) URN Namespace 的 4.1.2. Layout and Byte Order 有结构图:



这个图有点小迷惑, 最上面的 0,1,2,3 不是表示位数,就是简单的表示 10 位数的值,9 之后就是 10, 11, 12 等等。

这图不太好理解,换一张手工画的图(UUID 10类型的 V4 版本):10类型和V4版本后续会解释



128 比特,16 个字节即 16 hexOctet,就被如下瓜分了。



要想完整理解这个 6 部分组成,必然要理解备注中被加粗的几个概念。保留位版本, 时间戳, 时钟序列 ,节点标志符


类型(变体) 和保留位


UUID 可以分为四种类型(变体),怎么识别是哪种类型(变体呢),UUID 有对应的 Variant 字段去标记,可以参见协议的 4.1.1. Variant部分。

variant 字段位于 UUID 的第 8 个字节 即 clock_seq_hi_and_reserved 部分的第 6-7 位。



以外所有其他位的含义都是依据 variant 字段中的比特位设置来解读的。从这个意义上讲,variant 字段更准确地说可以被称作类型字段;然而为了与历史文档兼容,仍沿用“variant”这一术语。

下表列出了 variant 字段可能的内容,其中字母"x"表示无关紧要或不关心的值:

  • Msb0(最高有效位 0):此为最高位。

  • Msb1:次高位。

  • Msb2:第三高位。



类型(变体)的标志符可以是 2 位也可是 3 位,本文围绕的的是 RFC4122: A Universally Unique IDentifier (UUID) URN Namespace 类型(变体), 即上面表格的第二行,其第三高位 为 x,表示该值并无意义,所以该版本只需要10 即可。

10开头的 hexDigit 十六进制数字字符,其只有四个值。

0b1000   => 80b1001   => 90b1010   => a0b1011   => b
复制代码


用简单的图示表示,就是 下面y的部分只会是这 四个值 89ab其中的某个值。xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx

简单测一测,



所以呢,一个RFC4122版本的 UUID 正宗不正宗,这么验证也是一种手段。


版本(子类型)


上面提到了 UUID 的类型(变体), 而这里版本,可以理解为某个类型(变体)下的不同子类型。 当然本文讨论的是 变体 10 即RFC4122 下的版本(子类型)。 UUID 的类型(变体)有字段标记,当然 这里的版本也有。

即版本号 time_hi_and_version 的第 12 至 15 位



V4 版本如下:


一共有 5 个版本:



用简单的图示表示,就是 下面V的部分只会是这 五个值 12345其中的某个值。xxxxxxxx-xxxx-V xxx-yxxx-xxxxxxxxxxxx

借用 uuid 库演示一下:



时间戳

先回顾一下两张图




第一张是 UUID 各部分的组成,time_low ,time_mid, time_hi_and_version 包含了时间戳的不同部分。

第二张是 UUID 的五个版本,但是只有 V1 和 V2 提到了时间戳,也确实是这样,除了 V1 和 V2 版本真正用了时间戳,其余版本通过不同手段生成了数据填充了 time_low ,time_mid, time_hi_and_version 这三个部分。

那这个时间戳 是 开发者们 常用的 Date.now() 这个时间戳嘛, 答案当然不是。

这里的时间戳是一个 60 位长度的数值。对于 UUID 版本 1 和 2,它通过协调世界时(UTC)表示,即从 1582 年 10 月 15 日 0 点 0 分 0 秒开始算起的 100 纳秒间隔计数。

比如 2024 年 1 月 1 日 0 时 0 分 0 秒,这个值时间戳怎么算呢

const startOfUuidEpoch = new Date('1582-10-15T00:00:00.000Z');const uuidTimestampFromDate = (date) => {  // 直接计算给定日期距离UUID纪元开始的毫秒数  const msSinceUuidEpoch = date.getTime() - startOfUuidEpoch.getTime();
// 将毫秒转换为100纳秒的整数倍, 1 毫秒=1000000 纳秒 const uuidTimestampIn100Ns = Math.floor(msSinceUuidEpoch * 10000); // 每毫秒乘以10,000得到100纳秒
return uuidTimestampIn100Ns;};
// 计算2024年1月1日对应的UUID V1版本时间戳const targetDate = new Date('2024-01-01T00:00:00.000Z');const uuidV1Timestamp = uuidTimestampFromDate(targetDate); // 139233600000000000
复制代码


要保存为 60 位, 并划分高位(12),中间(16),低位三部分(32)

uuidV1Timestamp.toString(2).padStart(60,'0')// 000111101110101010000011100010110100110011001000000000000000

time-high time-mid time-low000111101110 1010100000111000 10110100110011001000000000000000
复制代码


在不具备 UTC 功能但拥有本地时间的系统中,只要在整个系统内保持一致,也可以使用本地时间替代 UTC。然而,这种方法并不推荐,因为仅需要一个时区偏移量即可从本地时间生成 UTC 时间。

对于 UUID 版本 3 或 5,时间戳是一个根据4.3 Algorithm for Creating a Name-Based UUID,由名称构建的 60 位值, V3 和 V5 区别是在算法上。

而对于 UUID 版本 4,时间戳则是一个随机或伪随机生成的 60 位值,具体细节参见第4.4 Algorithms for Creating a UUID from Truly Random or Pseudo-Random Numbers

小结一下,

  • 时间戳是即从 1582 年 10 月 15 日 0 点 0 分 0 秒开始算起的 100 纳秒间隔计数,是一个 60 位值,被分为 高位,中间,低位三部分填充到 UUID 中。

  • 只有 V1 和 V2 真正意义上用了时间戳

  • V3 和 V5 由名字构建而成的 60 位值

  • V4 随机或伪随机生成的 60 位值


时钟序列


时钟序列(clock sequence)用于帮助避免因系统时间被设置回溯或节点 ID 发生变化时可能出现的重复标识符。

举个实例,手动把系统的时间设置为一个过去的时间,那么就可能导致生成重复的 UUID.

协议考虑到了这点,就增加了时钟序列,增加一个变数,让结果不一样,当然如果序列也是不变的,那么还是可能重复,所以这个时钟序列也是会变化的。

如果系统时钟被设置为向前的时间点之前,或者可能已经回溯(例如,在系统关机期间),并且 UUID 生成器无法确定在此期间没有生成时间戳更大的 UUID,则需要更改时钟序列。若已知先前时钟序列的值,可以直接递增;否则应将其设置为一个随机或高质量的伪随机值。

同样,当节点 ID 发生变化(比如因为网络适配器在不同机器间移动),将时钟序列设置为随机数可以最大限度地降低由于各机器之间微小时间设置差异导致重复 UUID 的可能性。尽管理论上知道与变更后的节点 ID 关联的时钟序列值后可以直接递增,但这种情况在实际操作中往往难以实现。

时钟序列必须在其生命周期内首次初始化为随机数,以减少跨系统间的关联性。这提供了最大程度的保护,防止可能会快速在系统间迁移或切换的节点标识符产生问题。初始值不应与节点标识符相关联。

同样的,这个时间序列只在 V1 和 V2 是真的按照上面的规则或者约定来执行的。

对于 UUID 版本 3 或 5,时钟序列是一个由第4.3 Algorithm for Creating a Name-Based UUID节描述的名称构建的 14 位值。

而对于 UUID 版本 4,时钟序列则是一个如第4.4 Algorithms for Creating a UUID from Truly Random or Pseudo-Random Numbers节所述随机或伪随机生成的 14 位值。


节点标志符


空间唯一节点标识符,用来确保即便在同一时间生成的 UUID 也能在特定网络或物理位置上保持唯一性。

对于 UUID V1,这个节点标识符通常基于网络适配器的 MAC 地址或者在没有硬件 MAC 地址可用时由系统自动生成一个伪随机数。它的目的是反映生成 UUID 的设备在网络或物理空间中的唯一性,即使在相同的时序和时钟序列条件下,不同的设备也会因为其独特的节点标识符而产生不同的 UUID。

在 UUID V2 中,虽然不常用,但节点标识符的概念同样适用,用于标识系统的唯一性,只不过这里的“空间”更多地指向组织结构或其他逻辑意义上的空间划分。

总之,空间唯一节点标识符是为了保证在分布式系统环境下,即使时间戳相同的情况下也能生成唯一的 UUID,以区分不同物理节点上的事件或资源。

对于 UUID 版本 3 或 5: 节点字段(48 位)是根据第 4.3 节描述的方法,从一个名称构造而来。对于 UUID 版本 4: 节点字段(同样是 48 位)是一个随机或伪随机生成的值。


小结


从 V1 和 V2 版本来看, UUID 最后是想通过 时间和空间 上两层手段保证其唯一性:

  • 时间: 时间戳 + 时钟时序

  • 空间: 节点标志符(比如 MAC 地址)

同时考虑了 类型(变体) 和 版本(子类型),即下面这些组信息组成了 UUID

  • 时间戳

  • 时钟序列

  • 节点标志符

  • 保留位:即类型(变体)信息

  • 版本:V1 到 V5

因为保留位和版本信息本身是固定的,是可以从最后的 32 位 16 进制字符是可以直接或者间接看到的。

再回顾这张图,是不是比较清晰了



UUID 的 生成


协议中有具体描述 V1, V3 和 V5, 以及 V4 的基本流程或者约束。

v4


浏览器和 nodejs 内置的了 V4 的生成函数, 而且其生成规则相对简单。对应着协议 4.4. Algorithms for Creating a UUID from Truly Random or Pseudo-Random Numbers

版本 4 的 UUID 旨在通过真正的随机数或伪随机数生成 UUID。其生成算法相对简单,主要依赖于随机性:

生成算法步骤如下:

  1. 在 UUID 结构中的clock_seq_hi_and_reserved部分,将最高两位有效位(即第 6 位和第 7 位)分别设置为 0 和 1。

  2. 在 UUID 结构中的time_hi_and_version字段,将最高四位有效位(即第 12 位至第 15 位)设置为来自第 4.1.3节 的 4 位版本号,对于版本 4 UUID,这个版本号是固定的0100

  3. 将除了以上已设定的位之外的所有其他位设置为随机(或伪随机)选取的值。

不好理解,就看这张图:



关于随机性安全要求, 引用了BCP 106标准文档,即 RFC 4086。RFC 4086 是一份由 IETF 制定的最佳当前实践(Best Current Practice, BCP)文档,其标题为“Security Requirements for Randomness”,该文档详细阐述了在实现安全协议与系统时所需的随机数生成器的要求和特性,确保生成的随机数具有足够的不可预测性和熵,能满足各类安全应用,包括但不限于密码学应用中的随机性需求。

总之,生成版本 4 UUID 的过程中,首先对特定字段的几位进行固定设置以标明版本和时钟序列特征,然后其余所有位均通过随机或伪随机过程填充数值,以此确保生成的 UUID 具备全球唯一性和较强的随机性。

至于v2怎么生成,协议貌似没有提到, v1 , v3 和 v5均有提到,这边就直接翻译过来,有兴趣的可以看看大致逻辑。不敢兴趣的直接跳到后续章节


V1


对应这协议 4.2.2. Generation Details ,按照以下步骤生成的:

  1. 确定时间戳和时钟序列:遵循第 4.2.1 节描述的方法,获取基于 UTC 的时间戳以及用于 UUID 的时钟序列。

  2. 处理时间戳和时钟序列:将时间戳视为一个 60 位无符号整数,时钟序列视为一个 14 位无符号整数,并按顺序编号每个字段中的位,最低有效位从 0 开始计数。

  3. 设置时间低位字段(time_low field):将其设置为时间戳的最低有效 32 位(位 0 到 31),保持相同的位权重顺序。

  4. 设置时间中间字段(time_mid field):将其设置为时间戳中的位 32 到 47,同样保持位权重顺序一致。

  5. 设置时间高位及版本字段(time_hi_and_version field)的低 12 位(位 0 到 11):将其设置为时间戳的位 48 到 59,保持位权重顺序一致。

  6. 设置时间高位及版本字段的高 4 位:将这 4 位(位 12 到 15)设置为对应于所创建 UUID 版本的 4 位版本号。

  7. 设置时钟序列低位字段(clock_seq_low field):将其设置为时钟序列的最低有效 8 位(位 0 到 7),同样保持位权重顺序一致。

  8. 设置时钟序列高位及保留字段的低 6 位(clock_seq_hi_and_reserved field 的位 0 到 5):将其设置为时钟序列的最高有效 6 位(位 8 到 13),保持相同位权重顺序。

  9. 设置时钟序列高位及保留字段的高 2 位:将这 2 位(位 6 和 7)分别设置为 0 和 1,以满足版本 1 UUID 的标准格式要求。

  10. 设置节点字段(node field):将其设置为 48 位的 IEEE MAC 地址,地址中的每一位都保持原有的位权重顺序


V3 和 V5


对应协议的 4.3. Algorithm for Creating a Name-Based UUID

版本 3 或 5 的 UUID 设计用于从特定 命名空间(name space) 内的且在该命名空间内唯一的 名字(names) 生成 UUID。这里的名字(names)和命名空间(name space)的概念应该广泛理解,不仅限于文本名称。例如,一些命名空间包括域名系统(DNS)、统一资源定位符(URLs)、ISO 对象标识符(OIDs)、X.500 区别名(DNs)以及编程语言中的保留字等。在这些命名空间内分配名称和确保其唯一性的具体机制或规则不在本规范的讨论范围内。

对于这类 UUID 的要求如下:

  1. 在同一命名空间内,使用相同名称在不同时间生成的 UUID 必须完全相同。

  2. 在同一命名空间内,使用两个不同名称生成的 UUID 应当是不同的(概率极高)。

  3. 在两个不同命名空间内,使用相同名称生成的 UUID 也应当是不同的(概率极高)。

  4. 如果两个由名称生成的 UUID 相同,则它们几乎肯定是由同一命名空间内的相同名称生成的。

生成基于名称和命名空间的 UUID 的具体算法步骤如下:

  1. 为给定命名空间内所有由名称生成的 UUID 分配一个作为“命名空间 ID”的 UUID;参见附录C中预定义的一些值。

  2. 选择 MD5 [4] 或 SHA-1 [8] 其中的一种哈希算法;如果不考虑向后兼容性,建议优先使用 SHA-1。

  3. 将名称转换为其命名空间规定的标准化字节序列形式,并将命名空间 ID 以网络字节序排列。

  4. 计算命名空间 ID 与名称连接后的哈希值。

  5. 将哈希值的前四个八位组(octets 0-3)赋给时间低位字段(time_low field)的前四个八位组。

  6. 将哈希值的第五和第六个八位组赋给时间中间字段(time_mid field)的前两个八位组。

  7. 将哈希值的第七和第八个八位组赋给时间高位及版本字段(time_hi_and_version field)的前两个八位组。

  8. 将时间高位及版本字段的四位最显著位(bit 12 至 15)设置为第 4.1.3 节中指定的相应 4 位版本号。

  9. 将哈希值的第八个八位组赋给时钟序列高位及保留字段(clock_seq_hi_and_reserved field)。

  10. 将时钟序列高位及保留字段的两位最显著位(bit 6 和 7)分别设置为 0 和 1。

  11. 将哈希值的第九个八位组赋给时钟序列低位字段(clock_seq_low field)。

  12. 将哈希值的第十至第十五个八位组赋给节点字段(node field)的前六个八位组。

  13. 最后,将生成的 UUID 转换成本地字节序


获取 UUID V4


这里就只介绍 V4 版本,因为 V4 是基于 随机或者伪随机来实现的,只要保证 保留位 和 版本号 的固定,其他的随机生成就好。


正则 + Math.random


利用Math.random() 方法生成随机数。

function uuidv4() {  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {    var r = (Math.random() * 16 | 0), v = c == 'x' ? r : (r & 0b0011 | 0b1000);    return v.toString(16);  });}
复制代码


先固定好格式,执行 replace,整体代码不难,唯一需要提一下的是 (r & 0b0011 | 0b1000) 操作,这里的作用就是设置保留位的值10

r & 0b0011               // 高位,即2,3位 变为 00r & 0b0011 | 0b1000      // 高位,即2,3位 变为 10
复制代码


举个例子, 用 9 为例,其二进制 0b1001 &

0b1001 & 0b0011   => 0b00110b0011 | 0b1000   => 0b1011
复制代码


crypto.randomUUID

现代浏览器也内置 Crypto: randomUUID() method , nodejs 15.6.0 版本以上就内置了crypto.randomUUID([options])

crypto.randomUUID()// 4d93f326-3f48-4a43-929d-b6489f4754b5
复制代码


URL.createObjectURL

function uuid() {     const url = URL.createObjectURL(new Blob([]));     // const uuid = url.split("/").pop();     const uid = url.substring(url.lastIndexOf('/')+ 1);     URL.revokeObjectURL(url);     return uid; } uuid()// blob:http://localhost:3000/ff46f828-1570-4cc9-87af-3d600db71304
复制代码


上面方式产生的都是 v4 版本,如果 v4 版本满足需求,就没有必要去引入第三方库了。


你是否真的需要 UUID

在前端,有序后需要给数据添加一个 id 作为组件的 key,这时候理大多数情况是不需要 UUID, 也许下面的函数就满足了你的需求。

let id = 0;function getId () {    return id++;}
复制代码


后起之秀 NanoID


npm 网站, NanoID 是这么自我介绍的:

Nano ID 是一个精巧高效的 JavaScript 库,用于生成短小、唯一且适合放在 URL 中的标识符字符串。这个工具提供了几个关键特性:

  1. 体积小巧:Nano ID 的最小化和压缩版本非常紧凑,大小仅为 116 字节。

  2. 安全性:该库使用硬件随机数生成器来确保生成的 ID 具有高安全性,可以在集群环境中安全使用。

  3. 短小 ID:相较于 UUID(通常包含 A-Z、a-z、0-9 以及 - 符号,共 36 个字符),Nano ID 使用了更大的字符集(包括 A-Za-z0-9_-),从而将 ID 的长度从 36 个符号减少到了 21 个,更便于在有限空间中使用。

  4. 可移植性:Nano ID 已被移植到超过 20 种编程语言中,具有良好的跨平台适用性。

从最新的一周的下载量来对比,首先都是绝对的热门库,其次 NanoID 势头很盛。



借用 阿通 给的对比文案:

Nano ID 和 UUID(Universally Unique Identifier)都是用于生成唯一标识符的机制,但它们之间存在一些关键差异:

  1. 长度与格式

    UUID:标准 UUID 由 32 个十六进制数字组成,分为 5 组,每组之间用短横线-分隔,例如 xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx,总长度为 36 个字符(包括连字符)。

    Nano ID:Nano ID 可配置长度,但默认生成的是较短的字符串,通常包含 21 个字符,并且可以自定义字符集(默认为 A-Za-z0-9_-)。

  2. 唯一性保证

    UUID:基于时间戳、MAC 地址(对于 v1 UUID)、随机数(对于 v4 UUID)等多种因素生成,理论上全球范围内几乎不可能重复。

    Nano ID:虽然也致力于生成唯一的 ID,但由于其较短的长度,在没有额外存储或算法保证的情况下,唯一性风险相对较大。不过,通过增大字符集和适当增加 ID 长度,Nano ID 也能实现很高的唯一性概率。

  3. 应用场景

    UUID:广泛应用于数据库键、资源标识符、网络协议等需要全局唯一性的场景,尤其在网络间不同系统间的交互中常见。

    Nano ID:更适合于对 ID 长度要求严格的场合,如 URL 友好、前端显示或者存储空间有限的情况。

  4. 性能与存储成本

    UUID:由于较长的字符串长度,存储和传输时可能会占用更多空间。

    Nano ID:因其短小,Nano ID 在存储和带宽消耗上更有优势。

  5. 安全性

    UUID v4 是基于强随机性生成的,因此安全性较高,不易被预测。

    Nano ID 也可以使用安全的随机源生成,同样能够达到较高的安全性,但在默认设置下,考虑到生成长度和字符集的选择,如果不在生成逻辑上做特殊处理以增加熵,其安全性可能不及 UUID。


综上所述,选择 Nano ID 还是 UUID 取决于具体的应用需求,如果重视存储效率和简洁性,同时能接受合理的唯一性保证策略,则 Nano ID 可能更为合适;而在需要绝对唯一性和不考虑存储效率的场景下,UUID 往往是更好的选择。


文章转载自:-云-

原文链接:https://www.cnblogs.com/cloud-/p/18086034

体验地址:http://www.jnpfsoft.com/?from=001

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
深度解读UUID:结构、原理以及生成机制_前端_EquatorCoco_InfoQ写作社区