以randomUUID为例,揭秘JDK中构建UUID的原理


思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜


在之前的ULID:构建分布式ID的另一种选择中,笔者UUID的组成及构建原理进行了深入的分析和介绍,同时对UUID的平替ULID进行的阐述。而本文我们重新将视角聚焦到UUID的构建中,重点对JDK中构建UUID逻辑进行分析。

前言

正如在ULID:构建分布式ID的另一种选择中描述的那样,UUID通常是由3216进制表示形式下的字符组成,其一般表示为8-4-4-4-12。加上连接字符-一共有36个字符。

(图中各段的含义可参考往期内容,在此我们便不再赘述~)

JDK中与UUID相关API

JDK中为了方便方便快捷的使用UUID作为资源的唯一标识信息,其提供java.util.UUID类来帮助我们快速构建UUID。具体如下:

java 复制代码
public void testUUID() {
    log.info("generating UUID : [{}]", UUID.randomUUID());
}

运行上述代码我们即可生成一串名为:7e281cab-6b44-4426-bab1-f6a1405201d9UUID。不难发现,通过UUID.randomUUID()我们即可生成一串版本号为4UUID。正如我们之前的提到的在UUID规范中,对于UUID的版本主要有两种实现方式,一种是基于时间的version-1,另一种则是完全随机的version-4

说到此,你可能会有这样的疑惑,既然对于UUID而言其有两种版本的实现,那JDK中是对version-4的随机版本进行了实现吗?当然不是,在JDK中其大致提供了如下几种不同的版本信息:

  1. 时间戳UUID (Version 1) :这是最常见的UUID版本。它基于时间戳和节点信息生成,通常包含了时间戳、时钟序列号、节点标识等信息。
  2. DCE安全UUID (Version 2) :这个版本的UUID虽然包含了DCE(Distributed Computing Environment)的安全特性,但在开发中却很少用。
  3. 随机生成UUID (Version 3) :这个版本的UUID使用基于名称的UUID生成方法,通常将名称(如命名空间和名称字符串)和一个特定的算法作为输入生成UUID。这种方式不常用。
  4. 基于MD5散列的UUID (Version 4) :这是生成随机UUID的一种常用方式。它基于伪随机数生成器生成UUID,具有良好的随机性。也是UUID.randomUUID()方法默认使用的版本。
  5. 基于SHA-1散列的UUID (Version 5) :这个版本的UUID版本3类似,只不过其使用SHA-1散列算法生成,除此之外还通常也需要输入名称和命名空间作为参数。

UUID相关生成原理

接下来,我们以java.util.UUID.randomUUID() 为例来分析JDK中构建UUID的相关逻辑。

UUID.randomUUID()

java 复制代码
public static UUID randomUUID() {
    SecureRandom ng = Holder.numberGenerator;

    byte[] randomBytes = new byte[16];
    ng.nextBytes(randomBytes);
    randomBytes[6]  &= 0x0f;  /* clear version        */
    randomBytes[6]  |= 0x40;  /* set to version 4     */
    randomBytes[8]  &= 0x3f;  /* clear variant        */
    randomBytes[8]  |= 0x80;  /* set to IETF variant  */
    return new UUID(randomBytes);
}

不难发现,在randomUUID中其首先会构建一个SecureRandom对象。这里的这个SecureRandomJava中的一个类,其主要用于生成安全的伪随机数。看到随机数生成,可能你会很快想到Math库中的Random函数。虽然random函数也能生成随机数,但两者还是有差距的。

具体来说, SecureRandom是一个专门设计用于生成安全性强的伪随机数的工具类。其主要用于密码学、安全通信和安全相关应用。在SecureRandom会采用多重底层随机源,并使用强加密算法,以确保生成的随机数具有高度的随机性和不可预测性。而Random:普通的Random类生成的随机数质量不如SecureRandom,它使用一个伪随机数生成算法(通常是线性同余法)来生成的随机数,在某些情况下,其生成的结果其实是可以预测的。

而为了避免构建出重复的UUID,此处选用了SecureRandom来构建一个安全系数更高的随机数。当获取到一个随机数后,此时便会从SecureRandom中获取16个随机字节。然后接下来就是一通位运算操作了。

为了来分析清楚这些位运算操作到底做了哪些操作,接下来,假设ng.nextBytes(randomBytes)读取到的内容为1组成的随机串。即randomBytes中的内容为0xFF

首先,执行的是 randomBytes[6] &= 0x0f 也即 FF & 0F,所以经过操作后结果randomBytes[6] 0F,随后再执行0F|40的操作,不难发现经过这么一操作randomBytes[6] 存储的内容为4f。后续对于randomBytes[8]的操作则主要用于计算变体的相关信息。

(注:有关UUID变体的相关信息可参考ULID:构建分布式ID的另一种选择的相关分析)

总之,如果生成的16个随机字节全部都是 11111111,那么经过上述所示的位运算后,得到的UUID的版本和变体如下所示:

  • 版本: 0100
  • 变体: 10

剩下的操作就是将,它将SecureRandom生成的这16个随机字节组装成一个128位的UUID。即

  • 高64位:取前8个字节,即 FF FF FF FF FF FF FF FF
  • 低64位:取后8个字节,即 FF FF FF FF FF FF FF FF

最终,返回生成的UUID。因此当16个随机字节全部都是 11111111时,最终得到的UUID为: ffffffff-ffff-4fff-8fff-ffffffffffff

至于其中的8你可能会感到疑惑。接下来,我们来解释下8的由来。在分析之前首先明确一点,那就是8来自于UUID中的变体字段。在UUID变体字段通常占据了UUID13-16个字节。具体来说,UUID的第13个字节的高4位必须是固定的,而第13个字节的低4位是变化的,因此它将在0-15之间

但在生成UUID时,变体字段通常根据规范设置,以指示UUID的生成方法和结构。虽然变体字段的低4位可以是0到15之间的任何值,但在实际中,通常设置为10,表示UUID的生成方法是基于随机数的UUID。这也是为什么在示例UUID8的由来。

确定好了UUID中的版本信息和变体内容后,下一步要做的就是通过new UUID(randomBytes)来返回一个UUID实例对象。

接下来,我们来看看UUID的构造函数又完成了哪些操作,其内部相关代码如下:

UUID的构造函数

java 复制代码
// 通过长度为16的字节数组,计算mostSigBits和leastSigBits的值初始化UUID实例
private UUID(byte[] data) {
    long msb = 0;
    long lsb = 0;
    assert data.length == 16 : "data must be 16 bytes in length";
    for (int i=0; i<8; i++)
        msb = (msb << 8) | (data[i] & 0xff);
    for (int i=8; i<16; i++)
        lsb = (lsb << 8) | (data[i] & 0xff);
    this.mostSigBits = msb;
    this.leastSigBits = lsb;
}

上述代码中的变量mostSigBitsleastSigBits分别表示UUID的最高有效位和最低有效位。进一步,UUID够赞函数内部会通过二进制的移位操作来将data中字节数组的所有位就会转移到mostSigBitsleastSigBits之上,最后构成一个128UUID字符并将结果进行返回。

总结

Java中,UUID.randomUUID() 是一个我们常来构建UUID信息的方法。该方法内部的大致逻辑如下:

  1. 生成随机数 :在 randomUUID() 内部,其会借助SecureRandom强随机数生成器来产生随机数据,而这个随机数是用于构建新 UUID 的基础。

  2. 调整随机数以符合 UUID 规范 :根据UUID版本 4 的规范,其在构建uuid时需要提供版本号以及变体信息,这两个数据位置通常是固定的。即

    • 在第7位的高四位(固定为 0x4 以指示这是一个版本 4 的 UUID。
    • 在第 9 位的高两位,调整为 0x80x90xA0xB,以符合 RFC 4122 中对变体字段的要求。
  3. 使用私有构造函数创建 UUID 对象 :借助SecureRandom生成的随机数经调整后被分为两个 64 位数据,其分别代表UUID的最高有效位和最低有效位。并最终返回一个 UUID 实例。

如上就是 randomUUID() 每次调用后背后的逻辑,由于其基于强随机数生成器SecureRandom,因此生成的 UUID 在实践中具有非常高的唯一性和随机性,z这才使得它们适合用作数据库主键、对象标识符等。

至此,我们就对UUID内部的randomUUID方法进行了细致的分析。希望文章对你理解UUID有所帮助!

相关推荐
大P哥阿豪12 分钟前
Go defer(二):从汇编的角度理解延迟调用的实现
开发语言·汇编·后端·golang
今天背单词了吗98018 分钟前
算法学习笔记:11.冒泡排序——从原理到实战,涵盖 LeetCode 与考研 408 例题
java·学习·算法·排序算法·冒泡排序
Brookty26 分钟前
【操作系统】进程(二)内存管理、通信
java·linux·服务器·网络·学习·java-ee·操作系统
风象南26 分钟前
SpringBoot 与 HTMX:现代 Web 开发的高效组合
java·spring boot·后端
wstcl1 小时前
让你的asp.net网站在调试模式下也能在局域网通过ip访问
后端·tcp/ip·asp.net
倔强的小石头_3 小时前
【C语言指南】函数指针深度解析
java·c语言·算法
kangkang-7 小时前
PC端基于SpringBoot架构控制无人机(三):系统架构设计
java·架构·无人机
界面开发小八哥9 小时前
「Java EE开发指南」如何用MyEclipse创建一个WEB项目?(三)
java·ide·java-ee·myeclipse
ai小鬼头9 小时前
Ollama+OpenWeb最新版0.42+0.3.35一键安装教程,轻松搞定AI模型部署
后端·架构·github
idolyXyz9 小时前
[java: Cleaner]-一文述之
java