1. 概述

UUID 有不同的版本实现,通常我们说的 UUID 一般指的是UUID V4。本文主要讨论基于时间的UUID V1,以及最新的UUID v6 和v7版本。

我们将讨论基于时间的 UUID 的优点和缺点,以及何时应该选择它们。

最后我们学习如何在 Java 中使用第三方依赖库生成这些 UUID。

2. UUID 和基于时间的 UUID

UUID 是Universally Unique Identifier(通用唯一识别码)的缩写。它是一个 128 位标识符,每次生成时都应是唯一的。

UUID 不依赖中央机构的注册和分配,不分语言和平台,重复的概率接近零,可以忽略不计。

UUID 有不同的实现版本,其中UUID v1是基于时间 + 机器码(通常为MAC地址)生成的。最新的UUID V6、UUID v7 也是基于时间生成的,后面我们会讨论。

v1 版本具有几个优点

  • 按时间排序的 ID 更适合作为数据库的主键
  • 包含创建时间戳有助于分析和调试。

v1 版本缺点

  • 从同一主机生成多个 ID 时发生冲突的可能性略高。我们稍后会看到这是否是一个问题。
  • MAC 地址的暴露会造成了隐私与安全问题,这就是UUID v6试图提高安全性的原因。

3. 基准测试

为了更直观的对比说明,下面我们编写一个测试程序来比较 UUID 发生重复概率及 UUID 的生成耗时。

初始化变量:

int threadCount = 128;
int iterationCount = 100_000; 
Map<UUID, Long> uuidMap = new ConcurrentHashMap<>();
AtomicLong collisionCount = new AtomicLong();
long startNanos = System.nanoTime();
CountDownLatch endLatch = new CountDownLatch(threadCount);

我们启动 128 个线程,每个线程进行 100000 次迭代,使用 ConcurentHashMap 保存生成的 UUID。collisionCount 记录UUID冲突的次数。

for (long i = 0; i < threadCount; i++) {
    long threadId = i;
    new Thread(() -> {
        for (long j = 0; j < iterationCount; j++) {
            UUID uuid = UUID.randomUUID();
            Long existingUUID = uuidMap.put(uuid, (threadId * iterationCount) + j);
            if (existingUUID != null) {
                collisionCount.incrementAndGet();
            }
        }
        endLatch.countDown();
    }).start();
}

使用 CountDownLatch await() 方法等待所有线程执行完毕。

endLatch.await();
System.out.println(threadCount * iterationCount + " UUIDs generated, " + collisionCount + " collisions in "
        + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos) + "ms");

下面是使用 JDK 内置的 UUID 生成器测试结果:

12800000 UUIDs generated, 0 collisions in 4622ms

4622毫秒,生成1280万 UUID,0冲突。下面我们将与第三方库进行比较

4. 使用 UUID Creator

4.1. Maven 依赖

下面我们使用 UUID Creator 库生成UUID,导入maven 依赖:

<dependency>
    <groupId>com.github.f4b6a3</groupId>
    <artifactId>uuid-creator</artifactId>
    <version>5.2.0</version>
</dependency>

4.2. 使用方法

提供三种方式生成基于时间的 UUID,分别对应 UUID v1, v6 和v7:

  • UuidCreator.getTimeBased() – v1 版本
  • UuidCreator.getTimeOrdered() – v6 版本
  • UuidCreator.getTimeOrderedEpoch() – v7 版本

使用方法:

System.out.println("UUID Version 1: " + UuidCreator.getTimeBased());
System.out.println("UUID Version 6: " + UuidCreator.getTimeOrdered());
System.out.println("UUID Version 7: " + UuidCreator.getTimeOrderedEpoch());

打印结果:

UUID Version 1: 0da151ed-c82d-11ed-a2f6-6748247d7506
UUID Version 6: 1edc82d0-da0e-654b-9a98-79d770c05a84
UUID Version 7: 01870603-f211-7b9a-a7ea-4a98f5320ff8

对于 getTimeBased(),即UUID v1 它由3部分组成:时间戳,时钟序列,节点标识

Time-based UUID structure

 00000000-0000-v000-m000-000000000000
|1-----------------|2---|3-----------|

1: timestamp
2: clock-sequence
3: node identifier

4.3. 基准测试

使用 UuidCreator.getTimeBased() 做基准测试:

12800000 UUIDs generated, 0 collisions in 2595ms

可以看出效率要比 JDK 的高,详细benchmarks请参考官网。

5. 使用 JUG 库

5.1. Maven 依赖

下面我们使用另一个库: Java UUID Generator (JUG)

<dependency>
    <groupId>com.fasterxml.uuid</groupId>
    <artifactId>java-uuid-generator</artifactId>
    <version>4.1.0</version>
</dependency>

5.2. 使用方法

该库同样它支持v1-v7版本:

System.out.println("UUID Version 1: " + Generators.timeBasedGenerator().generate());
System.out.println("UUID Version 6: " + Generators.timeBasedReorderedGenerator().generate());
System.out.println("UUID Version 7: " + Generators.timeBasedEpochGenerator().generate());

生成结果:

UUID Version 1: e6e3422c-c82d-11ed-8761-3ff799965458
UUID Version 6: 1edc82de-6e34-622d-8761-dffbc0ff00e8
UUID Version 7: 01870609-81e5-793b-9e4f-011ee370187b

5.3. 基准测试

性能测试

UUID uuid = Generators.timeBasedGenerator().generate();

运行结果:

12800000 UUIDs generated, 0 collisions in 15795ms

可以看到这3种方式都没有出现重复的UUID,速度也不是瓶颈,虽然最后一个库要慢一点。

6. 总结

本文我们讨论了基于时间UUID的优缺点,并学习如何使用Java库生成UUID v6 和UUID v7。

最后,本文的源码可在GitHub上获取。