以randomUUID为例,揭秘JDK中构建UUID的原理
思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。 作者:毅航😜
前言
(图中各段的含义可参考往期内容,在此我们便不再赘述~)
JDK
中与UUID
相关API
JDK
中为了方便方便快捷的使用UUID
作为资源的唯一标识信息,其提供java.util.UUID
类来帮助我们快速构建UUID
。具体如下:
public void testUUID() {
log.info("generating UUID : [{}]", UUID.randomUUID());
}
运行上述代码我们即可生成一串名为:7e281cab-6b44-4426-bab1-f6a1405201d9
的UUID
。不难发现,通过UUID.randomUUID()
我们即可生成一串版本号为4
的UUID
。正如我们之前的提到的在UUID
规范中,对于UUID
的版本主要有两种实现方式,一种是基于时间的version-1
,另一种则是完全随机的version-4
。
说到此,你可能会有这样的疑惑
,既然对于UUID
而言其有两种版本的实现,那JDK
中是对version-4
的随机版本进行了实现吗?当然不是,在JDK
中其大致提供了如下几种不同的版本信息:
- 时间戳UUID (Version 1) :这是最常见的
UUID
版本。它基于时间戳和节点信息生成,通常包含了时间戳、时钟序列号、节点标识等信息。 - DCE安全UUID (Version 2) :这个版本的
UUID
虽然包含了DCE(Distributed Computing Environment)
的安全特性,但在开发中却很少用。 - 随机生成UUID (Version 3) :这个版本的
UUID
使用基于名称的UUID
生成方法,通常将名称(如命名空间和名称字符串)和一个特定的算法作为输入生成UUID
。这种方式不常用。 - 基于MD5散列的UUID (Version 4) :这是生成随机UUID的一种常用方式。它基于伪随机数生成器生成UUID,具有良好的随机性。也是
UUID.randomUUID()
方法默认使用的版本。 - 基于SHA-1散列的UUID (Version 5) :这个版本的
UUID
与版本3
类似,只不过其使用SHA-1
散列算法生成,除此之外还通常也需要输入名称和命名空间作为参数。
UUID
相关生成原理
接下来,我们以java.util.UUID.randomUUID()
为例来分析JDK
中构建UUID
的相关逻辑。
UUID.randomUUID()
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
对象。这里的这个SecureRandom
是Java
中的一个类,其主要用于生成安全的伪随机数
。看到随机数生成,可能你会很快想到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]
的操作则主要用于计算变体
的相关信息。
总之,如果生成的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
变体字段通常占据了UUID
的13-16
个字节。具体来说,UUID
的第13
个字节的高4
位必须是固定的,而第13
个字节的低4
位是变化的,因此它将在0-15之间
。
但在生成UUID
时,变体字段通常根据规范设置,以指示UUID
的生成方法和结构。虽然变体字段的低4
位可以是0到15
之间的任何值,但在实际中,通常设置为10
,表示UUID
的生成方法是基于随机数的UUID
。这也是为什么在示例UUID
中8
的由来。
确定好了UUID
中的版本信息和变体内容后,下一步要做的就是通过new UUID(randomBytes)
来返回一个UUID
实例对象。
接下来,我们来看看UUID
的构造函数又完成了哪些操作,其内部相关代码如下:
UUID
的构造函数
// 通过长度为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;
}
上述代码中的变量mostSigBits
和leastSigBits
分别表示UUID
的最高有效位和最低有效位。进一步,UUID
够赞函数内部会通过二进制的移位操作来将data
中字节数组的所有位就会转移到mostSigBits
和leastSigBits
之上,最后构成一个128
的UUID
字符并将结果进行返回。
总结
在Java
中,UUID.randomUUID()
是一个我们常来构建UUID
信息的方法。该方法内部的大致逻辑如下:
-
生成随机数:在
randomUUID()
内部,其会借助SecureRandom
强随机数生成器来产生随机数据,而这个随机数是用于构建新UUID
的基础。 -
调整随机数以符合 UUID 规范:根据
UUID
版本4
的规范,其在构建uuid
时需要提供版本号以及变体信息,这两个数据位置通常是固定的。即- 在第
7
位的高四位(固定为0x4
以指示这是一个版本 4 的 UUID。 - 在第
9
位的高两位,调整为0x8
、0x9
、0xA
或0xB
,以符合 RFC 4122 中对变体字段的要求。
- 在第
-
使用私有构造函数创建 UUID 对象:借助
SecureRandom
生成的随机数经调整后被分为两个 64 位数据,其分别代表UUID
的最高有效位和最低有效位。并最终返回一个UUID
实例。
如上就是 randomUUID()
每次调用后背后的逻辑,由于其基于强随机数生成器SecureRandom
,因此生成的 UUID
在实践中具有非常高的唯一性和随机性,z这才使得它们适合用作数据库主键、对象标识符等。
至此,我们就对UUID
内部的randomUUID
方法进行了细致的分析。希望文章对你理解UUID
有所帮助!
转载自:https://juejin.cn/post/7313805883590524954