在 Java 中实现只有数组和字母的 UUID

通用唯一识别码 (英语:Universally Unique Identifier,缩写:UUID )是用于计算机体系中以识别信息的一个128位标识符。

UUID 按照标准方法生成时,在实际应用中具有唯一性,且不依赖中央机构的注册和分配。UUID重复的概率接近零,可以忽略不计。

可以这样理解的是,UUID 就是一个可以在任何计算机中生成的一个识别码,这个识别码可以做到全球唯一,用来标识你的数据。

在本文章中,我们对 UUID 在 Java 中是如何实现来进行一些说明和解读。

结构

在对 UUID 进行说明之前,我们来看一个标准的 UUID。

下面就是一个标准的 UUID,使用横杠分隔符来进行分隔:

123e4567-e89b-42d3-a456-556642440000
xxxxxxxx-xxxx-Bxxx-Axxx-xxxxxxxxxxxx

标准的 UUID 目前是使用 32 个十六进制的字符来表示的。如果你对计算机中的 16 进制如何表示还不是非常熟悉的话,可以自行脑补下。

标准 UUID 使用横杠被分隔成了 5 组,分别为 8-4-4-4-12 个字符。总共这里有 32 个字符。因为我们使用了横杠,加上 4 个横杠后一共有 36 个字符。

Nil UUID 是一个特殊的 UUID,这个 UUID 中的所有字符都是 0,其值为:

 00000000-0000-0000-0000-000000000000

针对 UUID 上面的结构,每一位都会有具体的意义:

具体的分配如下图:

对大部分人来说,我们值需要了解下就可以了,因为我们多会使用 API 来进行生成,如果我们不在这里说明的话,在后面我们说版本的时候就比较难理解。

变形

版本号在时间戳的最高 4 位(time_hi_and_version 的 4-7 bit)

UUID 的变体规则如下:

  MSB1    MSB2    MSB3
   0       X       X     reserved (0)
   1       0       X     current variant (2)
   1       1       0     reserved for Microsoft (6)
   1       1       1     reserved for future (7)

针对微软的版本,UUID 还单独为微软保留了一个版本,这就可以说明为什么在 Azure 的平台上,微软那么喜欢用 UUID。

版本

在实际使用的时候,我们对 UUID 的版本其实并不关心。

目前大部分情况下使用默认的版本进行编程就可以了。

  1. Version 1 (基于时间):这个版本是基于随机数的,使用的基数为每 100 纳秒为一个单位,时间的起点为1582年10月15日。同时还需要加上当前计算机的网卡物理地址(MAC)。
  2. Version 2 (DCE – 分布式计算机环境): UUID-v2V1 很类似,是根据标识符(通常是组或用户ID )、时间和节点ID 生成,不过区别在于V2V1 中的部分时间信息换成了主机名, 故应用具有局限性(有隐私风险),未大规模使用。
  3. Version 3 (基于命名): UUID-v3 通过散列(MD5 )名字空间(namespace )标识符和名称生成。和V1V2 不同,V3 不依赖与机器信息和时间信息, 但是V3 要求输入命名空间+名称,命名空间本身也是一个UUID ,用来标识应用环境,名称通常是用户账号、用户名之类的内容,通过命名空间+名称+三列算法算出UUID
  4. Version 4 (基于随机数): UUID-v4 组成 UUID v4 的位是随机生成的,没有固有逻辑(除了第三段首个数字,该数字标识版本号),不包含命名空间、设备信息、时间信息。 故,UUID-v4 最容易理解、应用也最为广泛。
  5. Version 5 (基于使用 SHA-1 的命名): UUID-v5V3 类似,区别在于散列算法,使用了sha1 散列算法。

因为当前 MD5 的散列算法已经不推荐大规模使用了,所以如果你还需要使用 UUID V3 的话,会推荐使用 V5。

SHA-1 散列算法是当前被建议替代 MD5 使用的算法。

UUID

Java 已经在自己的 JDK 中集成了 UUID 的算法,你可以直接使用 UUID 的构造函数来生成一个随机的 UUID。

UUID 有一个单独的构造函数来为你创建 UUID。

UUID uuid = new UUID(long mostSignificant64Bits, long leastSignificant64Bits);

上面的构造函数需要你提供 2 个 long 类型的参数,在实际使用的时候这个就比较麻烦了。

为了简化使用,UUID 还提供了 3 个静态方法来让你更快的创建 UUID 对象。

使用下面的方法来创建一个 V3 版本的 UUID,这个时候你需要提供一个 buytes 的数组。

UUID uuid = UUID.nameUUIDFromBytes(byte[] bytes);

下面这个方法是我们用得最多的,大部分情况下我们都会使用下面的这个方法,这个方法将会生成一个 V4 的 UUID,你不需要为这个方法提供任何参数:

UUID uuid = UUID.randomUUID();

还有一个静态方法就是通过提供一个 UUID 字符串来生成一个新的 UUID。

UUID uuid = UUID.fromString(String uuidHexDigitString);

下面来让我们看下,如何不使用 JDK 中给定的 UUID 方法来实现我们需要的 UUID。

实现

针对 UUID 的实现,我们分成 2 类。

针对只需要 UUID 的情况,我们实现使用 UUIDv1UUIDv4 通常是最优的选择。

如果你的计算机是基于相同名字的,那么你应该选择使用 UUIDv3 或者 UUIDv5

因为,RFC 4122 的标准中没有针对 UUIDv2 进行说明,所以我们就不在这里对实现进行讨论的。

同时也是因为 UUIDv3 有很大的局限性,在实际使用情况下并没有被大规模推广,所以目前属于基本忽略不计的状态。

UUIDv1UUIDv4

针对 UUIDv1 这个版本,如果你担心你的 MAC 地址被泄露的话,你可以使用一个随机 48-bit 数组来替换 MAC 地址。

首先,我们先生成第一个 64 的值,并返回 long

    private static long get64LeastSignificantBitsForVersion1() {
        final long random63BitLong = new Random().nextLong() & 0x3FFFFFFFFFFFFFFFL;
        long variant3BitFlag = 0x8000000000000000L;
        return random63BitLong | variant3BitFlag;
    }

    private static long get64MostSignificantBitsForVersion1() {
        final long currentTimeMillis = System.currentTimeMillis();
        final long time_low = (currentTimeMillis & 0x0000_0000_FFFF_FFFFL) << 32;
        final long time_mid = ((currentTimeMillis >> 32) & 0xFFFF) << 16;
        final long version = 1 << 12;
        final long time_high = ((currentTimeMillis >> 48) & 0x0FFF);
        return time_low | time_mid | version | time_high;
    }

在生成上面 2 个值之后,我们在使用的 JDK 的 UUID 算法来生成 generateType1UUID。

public static UUID generateType1UUID() {

    long most64SigBits = get64MostSignificantBitsForVersion1();
    long least64SigBits = get64LeastSignificantBitsForVersion1();

    return new UUID(most64SigBits, least64SigBits);
}

下面我们来看看 UUIDv4 的生成算法,这个算法是我们目前最常用的算法了。

在这个算法中,JDK 实现了 SecureRandom 这个随机数生成器,通过这个函数通常用来降低可能出现重复的可能性。

UUID uuid = UUID.randomUUID();

上面的算法就是 JDK 中对 UUIDv4 实现的方法。

因为简单,所以被大规模使用。

UUIDv3UUIDv5

这个 UUID 的生成使用的使用命名空间和名字的散列算法。

命名空间使用类似下面的定义 UUIDs like Domain Name System (DNS), Object Identifiers (OIDs), 和URLs。

让我们来看下这个算法的伪代码:

UUID = hash(NAMESPACE_IDENTIFIER + NAME)

UUIDv3UUIDv5 的不同在于使用的散列算法的不同,UUIDv3 使用的是 MD5 (128 bits) 的散列算法, UUIDv5 使用的是 SHA-1 (160 bits) 的散列算法。

基于上面 2 个版本的实现我们就不再在这里具体讨论了,因为用得不多,可以在有时间的时候做下研究,在实际开发的时候可能不会被采用。

结论和总结

在本文章中,我们对 Java 实现的 UUID 算法进行了一些说明。

需要注意的时候,我们大部分情况使用的 UUID 版本是 UUIDv4,主要原因还是因为简单,适合使用。

针对一个相对独立的命名空间,可能会被要求使用 UUIDv3UUIDv5,如果被要求必须使用 UUIDv3UUIDv5 的话,请使用 UUIDv5