概述 在本文中,我们将会 对 UUIDs 和基于时间的 UUIDs(time-based UUIDs) 进行一些探讨。 当我们在对基于时间的 UUIDs 进行选择的时候,总会遇到一些好的方面和不好的方面,如何进行选择,也是我们将要简要探讨的内容。 同时我们还会对可能会使用类库进行一些比较和探索,以便于我们更好的做出选择。 UUIDs 和 基于时间的 UUIDs UUID 的全称是 Universally Unique Identifier,中文为通用唯一识别码。 当生成 UUID 的时候,系统总会自动生成一个 128 位的 UUID。基于 UUID 的生产算法的不同,我们会有不同的版本。 UUID 的主要目的就是用来在全世界中唯一标识一个数据,而且需要保证生成的 UUID 在全世界范围内是不重复的。因此我们可以用来标识一个上下文,包括数据库系统,计算机系统中的消息,分布式系统中的对象等等。 为了实现这个目标,我们需要确保哪怕是在同一个时间瞬间生成的 UUID 也是不相同,这样能够让我们更好的利用 UUID 在分布式计算机系统中标识存在的对象。 基于时间的 UUID,通过字面就可以了解到,这个 UUID 是基于时间的,实际上这个 UUID 存在 UUID 设计中的第一版。 这个版本是基于随机数的,使用的基数为每 100 纳秒为一个单位,时间的起点为1582年10月15日。同时还需要加上当前计算机的网卡物理地址(MAC)。 在后续的版本中,UUID (v6 和 v7)也是基于时间的 UUID 生成算法,可以说是基于 UUID v1 的更新版本。 UUID v1 因为是基于时间的,所以具有排序功能,这个在对数据库的设计上就很有帮助,当我们使用 UUID v1 来作为 PK(主键)的时候,我们就知道了,我们创建的这条记录的时间戳是什么时候,这个对我们在对数据进行调试和问题分析的时候就很有帮助了。 有优势就自然会有劣势,因为我们是基于时间创建 UUID 的,那么在同一个系统产生 UUID 冲突的可能性就会大很多,假设在同一个时间点,我们创建了很多个 UUID,那么大概率就会有出现冲突,重复出现的情况。 在本文的后部分,我们会对这个可能出现的情况进行一些探索。 另外一个原因,就是在 UUID v1 版本中使用主机地址这种做法会潜在的增加系统的安全性问题。这就是 UUID v6 尝试希望解决的问题。 对比程序 为了对可能出现的 UUID 冲突进行演示。我们尝试使用程序来对比可能出现 UUID 冲突的可能性。 这个程序,将会创建 128 个线程,在每个线程中将会生成 100,000 个 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 个线程,在这 128 个线程中,我们会循环 100,000 次。 同时,我们还初始化了一个 ConcurentHashMap 把我们生成的 UUID 存储到 ConcurentHashMap 中。 同时,我们还会记录出现 UUID 冲突的次数。 为了记录程序的性能,我们对程序开始时间和程序的结束也都进行了存储。在最后我们定义了一个 latch 等待所有线程的执行完成。 当定义完成后变量后,我们就需要启动线程并对线程序进行执行。 for (long i = 0; i < threadCount; i++) { long threadId = i; new Thread(() -> { for (long j = 0; j < iterationCount; j++) { UUID uuid = Generators.timeBasedGenerator().generate(); Long existingUUID = uuidMap.put(uuid, (threadId * iterationCount) + j); if(existingUUID != null) { collisionCount.incrementAndGet(); } } endLatch.countDown(); }).start(); } 在 UUID 的创建过程中,我们使用了 fasterxml 包中的 Generators,这个 Generators 使用的是 java.util.UUID 类来创建的。 在创建 UUID v1 的使用,使用 fasterxml 是我们常用的做法。 当 UUID 创建后,我们就把创建好的 UUID 存储到 Map 中,UUID 为 map 的 Key,当我们的 UUID 重复出现冲突的时候,Map 将会提示错误,我们程序就会捕获这个错误,然后把出现错误的计数器 + 1。 endLatch.await(); System.out.println(threadCount * iterationCount + " UUIDs generated, " + collisionCount + " collisions in " + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos) + "ms"); 在程序的最后,CountDownLatch 的 await() 方法会等待所有的线程完成。 当所有线程完成后,我们就会把结果打印在计算机屏幕上。 下面就让我们开始对程序进行运行。 12800000 UUIDs generated, 0 collisions in 16913ms 上面出现了程序的运行结果,我们可以看到并没有出现…