生成基于时间的UUID

2025/03/17

1. 概述

在本文中,我们将了解UUID和基于时间的UUID。

我们将了解基于时间的UUID的优点和缺点以及何时选择它们

我们还将探索和比较一些可以帮助我们实现生成UUID的不同算法的库。

2. UUID和基于时间的UUID

UUID代表通用唯一标识符,它是一个128位标识符,每次生成时都应是唯一的。

我们用它们来唯一地标识某物,即使该物没有固有标识符。我们可以在各种需要唯一标识对象的环境中使用它们,例如计算机系统、数据库和分布式系统。

两个UUID相同的可能性非常小,从统计上来说是不可能的,这使得它们成为在分布式系统中识别对象的可靠方法。

基于时间的UUID(也称为版本1 UUID)是使用当前时间和生成UUID的计算机或网络特有的唯一标识符生成的。即使同时生成多个UUID,时间戳也能确保UUID是唯一的。

我们将在下面实现的库中找到与时间相关的两个新版本的标准(v6和v7)。

版本1具有几个优点-按时间排序的ID更适合作为表中的主键,并且包含创建时间戳有助于分析和调试。但它也有一些缺点-从同一主机生成多个ID时发生冲突的可能性略高,我们稍后会看看这是否是个问题。

此外,包含主机地址可能会存在一些安全漏洞,这就是标准第6版试图提高安全性的原因。

3. 基准

为了使我们的比较更加直接,让我们编写一个基准程序来比较碰撞的可能性和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个线程上运行基准测试,每个线程进行100,000次迭代。此外,我们将使用ConcurrentHashMap来存储生成的所有UUID。除此之外,我们将使用计数器来记录冲突。为了检查速度性能,我们在执行开始时存储当前时间戳,以便将其与最终时间戳进行比较。最后,我们声明一个CountDownLatch来等待所有线程完成。

初始化测试所需的所有变量后,我们将循环并启动每个线程:

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();
}

对于每行执行,我们将再次集成并开始使用java.util.UUID类生成UUID。我们将所有ID及其对应的计数插入到Map中,UUID将是Map的键。

因此,如果我们尝试在Map中插入现有UUID,put()方法将返回已存在的键。当我们获得重复的UUID时,我们将增加冲突计数。在迭代结束时,我们将减少CountDownLatch计数。

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

最后,我们将使用CountDownLatch类的await()方法等待所有线程完成。我们将打印基准测试的结果,其中包括生成的UUID数量、冲突数量和执行时间。

现在,让我们针对JDK的内置UUID生成器运行基准测试:

12800000 UUIDs generated, 0 collisions in 4622ms

我们可以看到,所有ID的生成都没有发生冲突。在以下部分中,我们将与其他生成器进行比较。

4. UUID Creator

4.1 依赖

Java UUIDCreator库对于生成UUID非常有用且灵活,它提供了各种生成UUID的选项,其简单的API使其易于在各种应用程序中使用。我们可以将该添加到我们的项目中:

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

4.2 使用

该库为我们提供了三种生成基于时间的UUID的方法:

  • UuidCreator.getTimeBased():RFC-4122中指定的基于公历纪元的时间版本
  • UuidCreator.getTimeOrdered():提议将公历时间排序的版本作为新的UUID格式
  • UuidCreator.getTimeOrderedEpoch():提议以Unix纪元作为新UUID格式的时间顺序版本

添加依赖项后,我们可以在代码中直接使用它们:

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格式:

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

本文将重点介绍使用传统版本1 UUID的getTimeBased()方法。它包含三部分:时间戳、时钟序列和节点标识符。

Time-based UUID structure

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

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

4.3 基准

在本节中,我们将运行上一节中的基准测试,但我们将使用UuidCreator.getTimeBased()方法生成UUID。之后,我们得到结果:

12800000 UUIDs generated, 0 collisions in 2595ms

我们可以看到,该算法还成功生成了所有没有重复的UUID。除此之外,它甚至比JDK的运行时间更短。这只是一个基本的基准测试,不过还有更详细的基准测试可用。

5. Java UUID Generator(JUG)

5.1 依赖

Java UUID Generator(JUG)是一组用于处理UUID的Java类。它包括使用标准方法生成UUID、高效输出、排序等,它根据UUID规范(RFC-4122)生成UUID。

要使用该库,我们应该添加Maven依赖

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

5.2 使用

该库还提供了三种方法来创建基于时间的UUID(经典版本1以及新版本6和7)。我们可以通过选择一种生成器,然后调用其generate()方法来生成它们:

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:

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生成替换为以下内容来测试发生碰撞的可能性:

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

运行代码后我们可以看到结果:

12800000 UUIDs generated, 0 collisions in 15795ms

从中我们可以看到,我们也没有得到重复的UUID,就像前面的例子一样。但同时,我们也看到了执行时间的差异。即使差异看起来很大,两个库都很快生成了许多ID。

该库的文档告诉我们生成速度不太可能成为瓶颈,并且根据包和API的稳定性进行选择更好。

6. 总结

在本教程中,我们了解了基于时间的UUID的结构、优点和缺点。我们使用两个最流行的UUID库在代码中实现了它们,然后对它们进行了比较。

我们发现,选择UUID或库的类型可能取决于我们的需求。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章