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

相关推荐
jiunian_cn2 分钟前
【Redis】zset数据类型相关指令
数据库·redis·缓存
betazhou21 分钟前
MySQL相关性能查询语句
android·数据库·mysql
jiunian_cn21 分钟前
【Redis】set数据类型相关指令
数据库·redis·缓存
墨染青竹梦悠然30 分钟前
基于Django+vue的图书借阅管理系统
前端·vue.js·后端·python·django·毕业设计·毕设
松涛和鸣34 分钟前
DAY69 Practical Guide to Linux Character Device Drivers
linux·服务器·arm开发·数据库·单片机·嵌入式硬件
怪兽毕设36 分钟前
基于Django的洗衣服务平台设计与实现
后端·python·django·洗衣服务平台
咩咩不吃草42 分钟前
Linux环境下MySQL的安装与使用与Navicat
linux·运维·数据库·mysql·navicat
Aloudata43 分钟前
NoETL 指标平台如何保障亿级明细查询的秒级响应?——Aloudata CAN 性能压测深度解析
数据库·数据分析·自动化·指标平台
maoku6644 分钟前
从关键词到语义:向量数据库如何让AI真正理解你的需求
数据库·人工智能
寻道码路44 分钟前
【MCP探索实践】Google GenAI Toolbox:Google开源的企业级AI数据库中间件、5分钟搞定LLM-SQL安全互联
数据库·人工智能·sql·开源·aigc