1. 概述

UUID(Universally Unique Identifier)是一个128位的数字,设计用于全局唯一标识。实际应用中,UUID适用于需要唯一标识的场景,例如为数据库表创建主键。

Java提供了long原始类型,这种数据类型对人类友好且易于理解。在许多情况下,使用64位的long就能提供足够的唯一性且碰撞概率极低。此外,MySQL、PostgreSQL等数据库已针对数值类型进行了优化。

本文将讨论如何使用UUID(特别是版本4)生成唯一的正长整数值

2. 生成唯一正长整数

这个场景面临一个有趣的挑战:UUID是128位的,而long只有64位。这意味着唯一性潜力会降低。我们将探讨如何通过随机生成的UUID获取唯一正长整数值,并评估这种方法的有效性。

2.1. 使用 getLeastSignificantBits()

UUID类的getLeastSignificantBits()方法返回UUID的最低64位。这意味着它只提供了128位UUID值的一半。

让我们在randomUUID()方法后调用它:

long randomPositiveLong = Math.abs(UUID.randomUUID().getLeastSignificantBits());

在后续方法中,我们将继续使用Math.abs()确保生成正值。

2.2. 使用 getMostSignificantBits()

类似地,UUID类的getMostSignificantBits()方法也返回64位long。区别在于它取的是128位UUID值中最高位的比特。

同样,我们在randomUUID()方法后链式调用:

long randomPositiveLong = Math.abs(UUID.randomUUID().getMostSignificantBits());

断言结果值为正(实际是非负,因为可能生成0):

assertThat(randomPositiveLong).isNotNegative();

3. 效果评估

让我们评估这种方法的有效性。通过以下几个因素分析:

3.1. 安全性与效率

Java中的UUID.randomUUID()使用SecureRandom类生成安全随机数来创建版本4 UUID。如果生成的UUID将用于安全或加密场景,这点尤为重要。

为评估使用UUID生成唯一正长整数的效率和相关性,查看源码:

public static UUID randomUUID() {
    SecureRandom ng = Holder.numberGenerator;

    byte[] randomBytes = new byte[16];
    ng.nextBytes(randomBytes);
    randomBytes[6]  &= 0x0f;  /* 清除版本位        */
    randomBytes[6]  |= 0x40;  /* 设置为版本4     */
    randomBytes[8]  &= 0x3f;  /* 清除变体位        */
    randomBytes[8]  |= (byte) 0x80;  /* 设置为IETF变体  */
    return new UUID(randomBytes);
}

该方法使用SecureRandom生成16个随机字节构成UUID,然后调整其中几个比特位来指定UUID版本(版本4)和变体(IETF)。

虽然UUID功能强大,但在这个特定场景下,更简单的方法就能达到目标。因此,替代方案可能效率更高且更合适。

此外,这种方法可能降低生成比特的随机性。

3.2. 唯一性与碰撞概率

尽管UUID v4有128位范围,但其中4位用于标识版本4,2位用于标识变体。我们知道,在UUID显示格式中,每个字符代表4位十六进制数字:

xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx

数字4表示4位UUID版本(此处为版本4)。字母'y'包含2位IETF变体。剩余x部分包含122个随机位。因此碰撞概率为1/2^122。

RFC概述了UUID v4的创建方式。为更清晰了解比特位分布,再次查看UUID.randomUUID()的实现:

randomBytes[6]  &= 0x0f;  /* 清除版本位        */
randomBytes[6]  |= 0x40;  /* 设置为版本4     */

可见在randomBytes[6]中,最高位(MSB)有4位被设为版本标记。因此MSB中只有60位真正随机。故MSB的碰撞概率为1/2^59。

添加Math.abs()后,由于正负值重叠,碰撞概率翻倍。因此MSB中正长整数的碰撞概率为1/2^58

randomBytes[8]中,最低位(LSB)有2位被设为IETF变体:

randomBytes[8] &= 0x3f; /* 清除变体位 */ 
randomBytes[8] |= (byte) 0x80; /* 设置为IETF变体 */

因此LSB中只有62位真正随机。故LSB的碰撞概率为1/2^61。

添加Math.abs()后,碰撞概率同样翻倍。因此LSB中正长整数的碰撞概率为1/2^60

可见碰撞概率极小。但若问UUID内容是否完全随机,答案是否定的

3.3. 适用性分析

UUID设计用于全局唯一性、标识、安全性和可移植性。对于生成唯一正长整数值,存在更高效的方法,此时使用UUID显得多余

虽然UUID.randomUUID()拥有128位长度,但我们看到实际只有122位是随机的,而Java的long类型仅处理64位。将128位UUID转换为64位长整数时,会损失部分唯一性潜力。如果唯一性至关重要,需权衡此取舍。

4. 使用 SecureRandom 作为替代方案

如果需要唯一的long值,更合理的方式是使用适当范围的随机数生成器(例如用SecureRandom生成唯一随机长整数)。这能确保在适当范围内获得唯一long值,避免像使用UUID时那样损失大部分唯一比特。

SecureRandom secureRandom = new SecureRandom();
long randomPositiveLong = Math.abs(secureRandom.nextLong());

其碰撞概率也更低,因为它生成完全随机的64位long值。

为确保正值,只需添加Math.abs()。因此碰撞概率计算为1/2^62。十进制形式下,此概率约为0.000000000000000000216840434497100900。对大多数实际应用,可认为此概率微不足道

5. 结论

总之,尽管UUID提供全局唯一标识符,但用于生成唯一正长整数值时可能并非最高效选择,因为存在显著的比特位损失

使用getMostSignificantBits()getLeastSignificantBits()等方法仍能保持低碰撞概率,但直接使用SecureRandom等随机数生成器可能更高效且更适合生成唯一正长整数值

完整源代码见GitHub仓库


原始标题:Generating Unique Positive long Using UUID in Java