ULID:构建分布式ID的另一种选择


思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜


在现代应用程序开发中,为资源设计一个不会重复的唯一编码是很常见的一种需求。而目前生成唯一性ID常见的方式有:UUID、雪花算法等常见的方式。而ULID作为一个新兴的标识符生成算法,一定程度上可以作为UUID的平替。今天我们就来谈一谈UUIDULID两者间的区别。

前言

想象一下,如果宇宙中的每个星系、每颗星球、每个人都需要一个独一无二的身份证号码,那会是什么样的?是不只要我们记住这个个唯一的ID我们就能从浩瀚的星辰中快速找到那个我们所需要的?

类似的,在我们的日常开发中,为每个对象、每条记录、每个实体提供一个唯一的标识符是最常见的一个业务需求。而为了确保在全球范围内的唯一性,我们通常会使用UUID来为数据生成唯一的标识信息。

UUID简介

虽然日常工作中我们经常会使用UUID来作为资源的唯一性标示。但你是否有了解过UUID内部是如何来保证标识唯一的特性的呢?不了解也别着急,接下来我们就对UUID的组成和生成原理进行一个简单介绍。

通常,UUID的标准形式由32个十六进制数字组成,以连字符分为8-4-4-4-12五段的格式。

  1. 第一段(8个十六进制数字) :时间戳的低位部分。

  2. 第二段(4个十六进制数字) :时间戳的中间部分。

  3. 第三段(4个十六进制数字)

    • 前两位表示UUID版本(目前共有5种版本)。
    • 后两位通常是时间戳的高位部分。
  4. 第四段(4个十六进制数字)

    • 前两位用于表示"时钟序列"的高位部分,这是为了处理时钟回拨的问题。
    • 后两位是时钟序列的低位部分。
  5. 第五段(12个十六进制数字) :这部分通常是基于机器的MAC地址或随机生成。

接下来我们以123e4567-e89b-12d3-a456-426655440000为例,来看看在一串UUID中各段内容所标示的具体含义。

  1. 第一段(123e4567):这是时间戳的低位部分。

  2. 第二段(e89b):这也是时间戳的一部分。

  3. 第三段(12d3):

    • 1 表示UUID的版本(这里是版本1,即基于时间的UUID)。
    • 2d3 是时间戳的高位部分。
  4. 第四段(a456):这是属于计算机的"时钟序列",其主要用于处理在同一时间生成的UUID的可能性。

  5. 第五段(426655440000):这个值通常是基于机器的MAC地址或随机生成,以确保空间上的唯一性。

在上述例子中,时间戳部分是由 123e4567-e89b-2d3 组成的。这个时间戳表示自1582年10月15日以来所间隔的毫秒数。

其实UUID截止到目前有两个版本,一种就是基于时间机制生成的版本1。另一种则是完全随机生成的版本UUID-4。进一步,UUID版本4中其包括版本位变体位两个重要部分。

  • 版本位 :通常在UUID的第13个字符处会将版本信息设定为4
  • 变体位 :在UUID的第17个字符(十六进制表示的第9个字节的高2位)中,设置一定的位来指示UUID的变体。通常这些位设置为89AB

该版本的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 Feerasta2016年提出。通俗来讲,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

通常ULID26个字符组成,并使用CrockfordBase32进行编码。上述生成的ULID中各组成部分含义如下:

  • 前10个字符 01AN4Z07BY 代表时间戳,这部分是基于毫秒级Unix时间(从1970年1月1日以来的毫秒数)编码的。这种时间戳表示使得ULID在生成时具有可排序性。
  • 后16个字符 79KA1307SR9X4MV3 是随机生成的或伪随机生成的,用于确保全局唯一性。

不难发现,UUIDULID主要有如下的区别:

  • 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位的数。这意味着它有1280和1。进一步,由于每个Base32编码的字符代表5位,因此128位的二进制数可以被转换成 128 / 5=25.65128Base32字符。实际上,由于不能有分数的字符,所以总数被向上舍入到26个字符。

正是因为这样所在在 ULID中的前10个字符表示50位的时间戳(毫秒级),这占据了前60位。而接下来的16个字符代表80位的随机数或伪随机数。

(注:在ULID中,实际上只使用了128位,最后两位被忽略或置零)

因此,尽管ULID是由26个字符表示的,但每个字符携带的信息量更大,从而使得整个ULID仍然能够包含128位的数据。这种编码方式既保持了ULID的紧凑性,又提供了足够的数据量以保证其唯一性和可排序性。

下面是一些流行的Java类库,用于快速生成和处理ULID

  1. ulid-creator
  2. java-ulid
  3. 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

相关推荐
monkey_meng3 分钟前
【Rust中的迭代器】
开发语言·后端·rust
余衫马6 分钟前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng10 分钟前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
阑梦清川40 分钟前
在鱼皮的模拟面试里面学习有感
学习·面试·职场和发展
瓜牛_gn2 小时前
mysql特性
数据库·mysql
奶糖趣多多3 小时前
Redis知识点
数据库·redis·缓存
CoderIsArt4 小时前
Redis的三种模式:主从模式,哨兵与集群模式
数据库·redis·缓存
paopaokaka_luck5 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
师太,答应老衲吧6 小时前
SQL实战训练之,力扣:2020. 无流量的帐户数(递归)
数据库·sql·leetcode
码农小旋风6 小时前
详解K8S--声明式API
后端