思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
在现代应用程序开发中,为资源设计一个不会重复的唯一编码是很常见的一种需求。而目前生成唯一性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-creator
java-ulid
okjava-util-id
注:本文样例中使用ulid-creator
进行生成,其依赖如下:
xml
<dependency>
<groupId>com.github.f4b6a3</groupId>
<artifactId>ulid-creator</artifactId>
<version>5.1.0</version>
</dependency>
总结
ULID
为开发者提供了生成全局唯一且可排序标识符的强大工具,适用于日常的需要唯一性编码的地方。其使数据排序和唯一性处理在数据库、分布式系统及日志记录等方面更加高效。如果你目前需要生成唯一性ID
,不妨一试ULID
。