目录
1 什么是UUID
1.1 UUID的定义
UUID(Universally Unique Identifier),翻译为中文是通用唯一识别码,UUID的目的是让分布式系统中的所有元素都能有唯一的识别信息。每个人都可以创建不与其他人冲突的UUID,就不需要考虑数据库创建的时候名称重复的问题;
UUID 是由一组32位数的16进制数字所构成,是故 UUID 理论上的总数为16^32=2^128,约等于3.4 x 10^123。即若每秒产生一百万个UUID,要花100亿年才会将所有的UUID用完。
UUID生成ID的时候,只考虑随机性或者是时间戳,生成一个36个字符的长字符串
1.2 UUID的组成
UUID的十六个八位字节被表示为32个十六进制的数字,以连字号分页的五组来显示,形式为8-4-4-4-12,总共有36个字符【32个英文、数字字母和四个连字号】
例如:
123e4567-e89b-12d3-a456-426655440000
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
数字 M 的四位表示 UUID 版本,当前规范有5个版本,M可选值为1,2,3,4,5;
数字 N 的一至四个最高有效位表示 UUID 变体( variant ),有固定的两位 10xx 因此只可能取值8, 9, a, b;
1.3 UUID的版本
UUID版本通过M表示,当前规范有5个版本,M可选值为1, 2, 3, 4, 5。这5个版本使用不同算法,利用不同的信息来产生UUID,各版本有各自优势,适用于不同情景。具体使用的信息
-
version 1, date-time & MAC address【日期时间和mac地址】
-
version 2, date-time & group/user id【日期时间和小组/用户id】
-
version 3, MD5 hash & namespace【MD5哈希值和命名空间】
-
version 4, pseudo-random number【伪随机数】
-
version 5, SHA-1 hash & namespace【安全散列算法1和命名空间】
SHA-1(英语:Secure Hash Algorithm 1,中文名:安全散列算法1)是一种密码散列函数。SHA-1可以生成一个被称为消息摘要的160位(20字节)散列值,散列值通常的呈现形式为40个十六进制数。
使用较多的是版本1和版本4,其中版本1使用当前时间戳和MAC地址信息。版本4使用(伪)随机数信息,128bit中,除去版本确定的4bit和variant(变体)确定的2bit,其它122bit全部由(伪)随机数信息确定。
因为时间戳和随机数的唯一性,版本1和版本4总是生成唯一的标识符。
若希望对给定的一个字符串总是能生成相同的 UUID,使用版本3或版本5。
Java中 UUID 使用版本4进行实现,所以由java.util.UUID类产生的 UUID,128个比特中,有122个比特是随机产生,4个比特标识版本被使用,还有2个标识变体被使用。
1.4 UUID存在的问题
-
UUID不是128 bit随机编码(由128 bit随机数通过编码生成字符串)的最高效实现方式
-
UUID的v1/v2实现依赖唯一稳定MAC地址不现实,v3/v4/v5实现因为随机性产生的ID会"碎片化"。
2 什么是ULID
ULID(Universally Unique Lexicographically Sortable Identifier)通用唯一词典分类标识符;ULID生成ID的时候,会同时考虑随机性和时间戳来生成id,并将他们编码为26个字符串(128位)
2.1 ULID的组成
ULID 的前10个字符表示时间戳,后16个字符表示随机性。这两个部分都是base 32编码字符串,分别使用48位和80位表示。
ULID规范的字符串表示形式的长度是确定的,共占据26个字符。
ULID规范的字符串表示形式如下:
ttttttttttrrrrrrrrrrrrrrrr
where
t is Timestamp (10 characters)【
时间戳】
r is Randomness (16 characters)【随机数】
例如:
ULID
01FHZXHK8PTP9FVK99Z66GXQTX
ULID分解如下
时间戳 (48 bits) - 01FHZXHK8P
随机数 (80 bits) - TP9FVK99Z66GXQTX
注意:
ULID的时间戳部分是以UNIX时间(以毫秒为单位)表示,知道公元10889年才会耗尽空间。
ULID 使用 Crockford 的 Base32 字母表 (0123456789ABCDEFGHJKMNPQRSTVWXYZ) 进行编码。它不包括 I、L、O 和 U 字母以避免任何意外的混淆。
2.2 ULID的特点
-
设计为128 bit大小,与UUID兼容
-
ULID是既基于时间戳又基于随机数,时间戳精确到毫秒,不存在冲突的风险,每毫秒生成1.21e+24个唯一的ULID(高性能)
-
按字典顺序(字母顺序)排序
-
标准编码为26个字符的字符串,而不是像UUID那样需要36个字符
-
使用Crockford base32算法来提高效率和可读性(每个字符5 bit)
-
不区分大小写
-
没有特殊字符串(URL安全,不需要进行二次URL编码)
-
单调排序(正确地检测并处理相同的毫秒,所谓单调性,就是毫秒数相同的情况下,能够确保新的ULID随机部分的在最低有效位上加1位)
词典可排序熊是ULID最突出的特点之一。最左边的字符必须排在最前面,最右边的字符必须排在最后(词汇顺序)。必须使用默认的ASCII字符集,在统一毫秒之内,不能保证排序顺序;
//单调排序举例:
monotonicUlid() // 01GGS5FGGZA4DPDE82PHAEB7SZ
monotonicUlid() // 01GGS5FGGZA4DPDE82PHAEB7T0
monotonicUlid() // 01GGS5FGGZA4DPDE82PHAEB7T1
monotonicUlid() // 01GGS5FGGZA4DPDE82PHAEB7T2
...
monotonicUlid() // 01GGS5FGGZA4DPDE82PHAEB7TZ
monotonicUlid() // 01GGS5FGGZA4DPDE82PHAEB7T0
可以看到上边的时间戳部分都为:01GGS5FGGZ,随机数部分只有最后以为一次增大,这就是
ULId的单调性
2.3 ULID的应用场景
-
替换数据库自增id,无需DB参与主键生成;
-
分布式环境下,替换UUID,全局唯一且毫秒精度有序;
-
如果要按照日期对数据库进行分区分表,可以使用ULID中嵌入的时间戳来选择正确的分区分表
-
如果毫秒精度内是可以接受的(毫秒内无序),可以按照ULID进行排序,二不是单独的created_at字段;
2.4 ULID的溢出错误处理
从技术实现上来看,26个字符的Base32编码字符串可以包含130 bit信息,而ULID只包含128bit的信息,所以可以使用Base32算法对ULID进行编码。
基于Base32编码算法能有生成的最大的合法的ULID数是:7ZZZZZZZZZZZZZZZZZZZZZZZZZ,并且使用的时间戳最大为纪元时间:281474976710655,即2^48-1。对于任何大于这个值的ULID进行解码或者编码的尝试都应该改被所有实现拒绝,以防止溢出错误。
ULID溢出错误测试:
如果时间戳时间大于最大值,则会出现java.lang.IllegalArgumentException: Invalid time value的错误;
2.5 ULID的二进制布局
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 32_bit_uint_time_high |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 16_bit_uint_time_low | 16_bit_uint_random |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 32_bit_uint_random |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 32_bit_uint_random |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
二进制布局的多个部分被编码为16 byte,每个部分都以最高字节优先(网络字节序,也就是big-endian)进行编码;
3 Java使用ULID
3.1 pom文件引入依赖:
<!-- https://search.maven.org/artifact/com.github.f4b6a3/ulid-creator -->
<dependency>
<groupId>com.github.f4b6a3</groupId>
<artifactId>ulid-creator</artifactId>
<version>5.1.0</version>
</dependency>
3.2 使用Demo
3.2.1 常规生成ULID
生成普通的ULID的方法:
-
UlidCreator.getUlid()
-
UlidCreator.getUlid(final Long time)【参数time – 自 1970-01-01(Unix 纪元)以来的毫秒数】
-
@Test void ulidTest(){ Ulid ulid = UlidCreator.getUlid(1670837655000l); Ulid ulid1 = UlidCreator.getUlid(1670837655000l); Ulid ulid2 = UlidCreator.getUlid(1670837655000l); log.info("返回结果:" + ulid + "***********"+ ulid1 + "*********" + ulid2); log.info("ULID时间:"+ String.valueOf(Ulid.getTime(ulid.toString())) + "***********"+ String.valueOf(Ulid.getTime(ulid1.toString())) + "***********"+ String.valueOf(Ulid.getTime(ulid2.toString()))); } 返回结果:01GM2TYNER7TMDSJCJRARCHP6A***********01GM2TYNER1CKWEFH0HDSR0YQB*********01GM2TYNERC7BH0VZ8G3NPJNCZ ULID时间:1670837655000***********1670837655000***********1670837655000
结论:
根据返回结果可以得到结果的时间戳部分是相同的,但是他的随机数部分是随机的没有排序顺序没有单调性;
根据已ULID时间可以得到这些ULID的时间是相同的;
3.2.2 生成单调排序ULID
生成单调排序的ULID的方法:
-
UlidCreator.getMonotonicUlid()
-
UlidCreator.getMonotonicUlid(final Long time)【参数time – 自 1970-01-01(Unix 纪元)以来的毫秒数】
@Test
void ulidMonotonicTest(){
Ulid monotonicUlid = UlidCreator.getMonotonicUlid(1670837655000l);
Ulid monotonicUlid1 = UlidCreator.getMonotonicUlid(1670837655000l);
Ulid monotonicUlid2 = UlidCreator.getMonotonicUlid(1670837655000l);
log.info("结果是:" + monotonicUlid + "***********"+ monotonicUlid1 + "*********" + monotonicUlid2);
log.info("ULID时间:"+ String.valueOf(Ulid.getTime(monotonicUlid .toString())) +
"***********"+ String.valueOf(Ulid.getTime(monotonicUlid1.toString())) +
"***********"+ String.valueOf(Ulid.getTime(monotonicUlid2.toString())));
}
返回结果:01GM2TYNERZ5GQDAG9Q7Y56YC3***********01GM2TYNERZ5GQDAG9Q7Y56YC4*********01GM2TYNERZ5GQDAG9Q7Y56YC5
ULID时间:1670837655000***********1670837655000***********1670837655000
结论:
根据返回结果可以得到结果的时间戳部分是相同的,它的随机数部分也是相同的,但是随机数部分的最后一位逐次+1,提现了ULID的单调性;
根据已ULID时间可以得到这些ULID的时间是相同的;
3.2.3 测试单调性ULID大小比较
方法:
compareTo();
@Test
void ulidSortTest(){
Ulid monotonicUlid = UlidCreator.getMonotonicUlid(1670837655000l);
Ulid monotonicUlid1 = UlidCreator.getMonotonicUlid(1670837655000l);
Ulid monotonicUlid2 = UlidCreator.getMonotonicUlid(1670837655000l);
log.info("结果是:" + monotonicUlid + "***********"+ monotonicUlid1 + "*********" + monotonicUlid2);
log.info(String.valueOf(monotonicUlid.compareTo(monotonicUlid1)));
log.info(String.valueOf(monotonicUlid.compareTo(monotonicUlid)));
log.info(String.valueOf(monotonicUlid2.compareTo(monotonicUlid)));
}
结果是:01GM2TYNERJGWQQ9SA4BYQ4Y7C***********01GM2TYNERJGWQQ9SA4BYQ4Y7D*********01GM2TYNERJGWQQ9SA4BYQ4Y7E
-1 (小于)
0 (等于)
1 (大于)
结论:
根据获取到的结果,可以可出他的单调ULID,也可以看出他的随机数部分是在最低位依次增大,之后使用compareTo方法也可以对其进行大小比较;
3.2.4 获取ULID的时间部分,随机数部分,创建时刻
方法:
获取时间:Ulid.getTime(String);
获取随机数:Ulid.getRandom(String);
获取创建时刻:Ulid.getInstant(String);
@Test
void getTime(){
Ulid monotonicUlid = UlidCreator.getMonotonicUlid();
log.info(monotonicUlid.toString());
String s = monotonicUlid.toString();
log.info("时间是:"+String.valueOf(Ulid.getTime(s)));
log.info("随机数部分是:"+String.valueOf(Ulid.getRandom(s)));
log.info("创建时刻是:"+String.valueOf(Ulid.getInstant(s)));
Date date = new Date(Ulid.getInstant(s).toEpochMilli());
long time = date.getTime();
log.info("日期时间是:"+date.toString());
log.info("时间戳是:"+String.valueOf(time));
}
时间是:1670837655000
随机数部分是:[B@7ecec90d
创建时刻是:2022-12-12T09:34:15Z
日期时间是:Mon Dec 12 17:34:15 CST 2022
结论:
根据代码运行结果可以得出,ULID可以通过方法获取到他的时间戳部分、随机数部分、以及创建ULID的时刻;
如果毫秒精度内是可以接受的(毫秒内无序),可以通过获取ULID的时间戳部分进行排序;