思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
在现代应用程序开发中,为资源设计一个不会重复的唯一编码是很常见的一种需求。而目前生成唯一性ID常见的方式有:UUID、雪花算法等常见的方式。而ULID作为一个新兴的标识符生成算法,一定程度上可以作为UUID的平替。今天我们就来谈一谈UUID和ULID两者间的区别。
前言
想象一下,如果宇宙中的每个星系、每颗星球、每个人都需要一个独一无二的身份证号码,那会是什么样的?是不只要我们记住这个个唯一的ID我们就能从浩瀚的星辰中快速找到那个我们所需要的?
类似的,在我们的日常开发中,为每个对象、每条记录、每个实体提供一个唯一的标识符是最常见的一个业务需求。而为了确保在全球范围内的唯一性,我们通常会使用UUID来为数据生成唯一的标识信息。
UUID简介
虽然日常工作中我们经常会使用UUID来作为资源的唯一性标示。但你是否有了解过UUID内部是如何来保证标识唯一的特性的呢?不了解也别着急,接下来我们就对UUID的组成和生成原理进行一个简单介绍。
通常,UUID的标准形式由32个十六进制数字组成,以连字符分为8-4-4-4-12五段的格式。
-
第一段(8个十六进制数字) :时间戳的低位部分。
-
第二段(4个十六进制数字) :时间戳的中间部分。
-
第三段(4个十六进制数字) :
- 前两位表示UUID版本(目前共有5种版本)。
- 后两位通常是时间戳的高位部分。
-
第四段(4个十六进制数字) :
- 前两位用于表示"时钟序列"的高位部分,这是为了处理时钟回拨的问题。
- 后两位是时钟序列的低位部分。
-
第五段(12个十六进制数字) :这部分通常是基于机器的
MAC地址或随机生成。
接下来我们以123e4567-e89b-12d3-a456-426655440000为例,来看看在一串UUID中各段内容所标示的具体含义。

-
第一段(
123e4567):这是时间戳的低位部分。 -
第二段(
e89b):这也是时间戳的一部分。 -
第三段(
12d3):1表示UUID的版本(这里是版本1,即基于时间的UUID)。2d3是时间戳的高位部分。
-
第四段(
a456):这是属于计算机的"时钟序列",其主要用于处理在同一时间生成的UUID的可能性。 -
第五段(
426655440000):这个值通常是基于机器的MAC地址或随机生成,以确保空间上的唯一性。
在上述例子中,时间戳部分是由 123e4567-e89b-2d3 组成的。这个时间戳表示自1582年10月15日以来所间隔的毫秒数。
其实UUID截止到目前有两个版本,一种就是基于时间机制生成的版本1。另一种则是完全随机生成的版本UUID-4。进一步,UUID版本4中其包括版本位和变体位两个重要部分。
- 版本位 :通常在
UUID的第13个字符处会将版本信息设定为4。 - 变体位 :在UUID的第17个字符(十六进制表示的第9个字节的高2位)中,设置一定的位来指示
UUID的变体。通常这些位设置为8、9、A或B。
该版本的UUID最终形式也遵循标准的8-4-4-4-12格式,例如:f47ac10b-58cc-4372-a567-0e02b2c3d479。除了上述提到的版本和变体位之外,其余的所有位都是随机生成的。
正是因为UUID在生成时的随机性,导致其在数据库排序时,无法充分利用索引机制,同时难于理解。而UUID通常无法排序的原因主要有如下几点:
- 时间戳位置 :在
UUID(特别是版本1)中,时间戳的信息被分散在整个UUID中,并且不是按照常规的时间顺序排列。时间戳的一部分放在UUID的开头,但其他部分散布在不同的段中,这使得UUID在整体上并不按照时间顺序排列。
(注:UUID版本4则完全随机,更不具有排序的可能性)
- 版本和变体标识 :
UUID中包含了版本和变体的标识符,这些也影响了UUID的整体排列的顺序。
为了解决这些问题ULID(Universally Unique Lexicographically Sortable Identifier)应运而生。
ULID概述
ULID(Universally Unique Lexicographically Sortable Identifier)是一种全局唯一的标识符,其由Alizain Feerasta于2016年提出。通俗来讲,ULID基于UUID(Universally Unique Identifier)和时间戳的形式,并采用了一种特殊的字母数字编码的方式,其可以将时间戳作为排序的依据。
如下是通过UlidCreator生成的一个ULID字符串的简单例子:
java
import com.github.f4b6a3.ulid.UlidCreator;
public class Main {
public static void main(String[] args) {
String ulid = UlidCreator.getUlid(); // 生成ULID
System.out.println("Generated ULID: " + ulid);
}
}
sheel
Generated ULID: 01AN4Z07BY79KA1307SR9X4MV3
通常ULID由26个字符组成,并使用Crockford的Base32进行编码。上述生成的ULID中各组成部分含义如下:
- 前10个字符
01AN4Z07BY代表时间戳,这部分是基于毫秒级Unix时间(从1970年1月1日以来的毫秒数)编码的。这种时间戳表示使得ULID在生成时具有可排序性。 - 后16个字符
79KA1307SR9X4MV3是随机生成的或伪随机生成的,用于确保全局唯一性。
不难发现,UUID和ULID主要有如下的区别:
- UUID :由
32个十六进制数字组成,通常表示为8-4-4-4-12的五段格式。它不是为了排序而设计的,因此在生成顺序上没有内在的可排序性。 - ULID :由
128位组成,通常使用Crockford的Base32编码,总共26个字符(全部大写)。前10个字符表示时间戳,后16个字符表示随机或伪随机值。这种结构使ULID在字典顺序上可排序。
讲到这,你可能会疑惑,ULID通过26个字符如何才占用128bit?不应该是208bit吗?这是因为ULID使用的是Crockford的Base32编码方案,这种编码方式允许每5 bits 被表示为一个字符。接下来,让我们来看看这是如何工作的:
首先, Base32编码是一种编码方法,它使用32个字符(通常是26个大写英文字母和6个数字)来表示二进制数据。而在Base32编码中,每5个位(bits)被编码成一个字符。这是因为 2^5 = 32,所以5位足以表示32种不同的值。然后,在二进制表示中,ULID是一个128位的数。这意味着它有128个0和1。进一步,由于每个Base32编码的字符代表5位,因此128位的二进制数可以被转换成 128 / 5=25.65128个Base32字符。实际上,由于不能有分数的字符,所以总数被向上舍入到26个字符。
正是因为这样所在在 ULID中的前10个字符表示50位的时间戳(毫秒级),这占据了前60位。而接下来的16个字符代表80位的随机数或伪随机数。
(注:在ULID中,实际上只使用了128位,最后两位被忽略或置零)
因此,尽管ULID是由26个字符表示的,但每个字符携带的信息量更大,从而使得整个ULID仍然能够包含128位的数据。这种编码方式既保持了ULID的紧凑性,又提供了足够的数据量以保证其唯一性和可排序性。
下面是一些流行的Java类库,用于快速生成和处理ULID:
ulid-creatorjava-ulidokjava-util-id
注:本文样例中使用ulid-creator进行生成,其依赖如下:
xml
<dependency>
<groupId>com.github.f4b6a3</groupId>
<artifactId>ulid-creator</artifactId>
<version>5.1.0</version>
</dependency>
总结
ULID为开发者提供了生成全局唯一且可排序标识符的强大工具,适用于日常的需要唯一性编码的地方。其使数据排序和唯一性处理在数据库、分布式系统及日志记录等方面更加高效。如果你目前需要生成唯一性ID,不妨一试ULID。