UUID(通用唯一识别码)嘛,就是类似这样“
e48364cd-e497-4c90-a775-a05c79bd9167”,因其理论上具有全局唯一性,做数据库的主键是很好的。
但又因为其无序性,再加上MySQL(InnoDB)中,主键的聚簇索引是B+树,必须保持有序,所以会导致频繁页分裂,影响写入性能,增加磁盘I/O,因此实践中主键还是用自增。
不过自增ID又有其自身的麻烦,导致项目实践中不拿它来做业务。例如:
- 自增ID很容易让攻击者按顺序爬数据。
- 关联表迁移的时候,丢失关系。
- 分库扩容要调整步长等,复杂易出错。
还有其他问题,不赘述了。
这时候要么是主键使用雪花算法ID,要么就图省事,主键还是自增,再多一个业务ID使用雪花算法(我也见过用UUID的)。
不过雪花算法也有问题,例如强依赖机器ID的配置,扩容的时候要协调,还有就是头疼的时钟回拨问题。
反正不管怎么做,没有完美的解决方案。
现在有了UUID V7,也并非完美,只是提供另一种思路。
介绍
不搞长篇大论,简单几点:
- 128位=48位时间戳(毫秒级,UTC时区)+4位版本号(固定值0111,代表版本7)+12位随机数A+2位变体号(固定值10,符合RFC 4122 标准)+62位随机数B。
- 时间戳保证有序(毫秒级)。有高精度时间(微秒级)需求,可以使用12位的随机数A进行填充。
- 随机数保证唯一,与时间戳组合后,每秒10亿次生产的数,需要数万亿年才可能重复。
- 去中心化,各节点独立生成。
注意时间戳,其天然可以作为create_time来用。
问题
UUID V7有个突出的问题就是其存储空间,如果是按照字符串CHAR(36)存储,在utf8mb4字符集下,要占144字节。
雪花ID用bigint,也就64位8个字节。
不过MySQL可以使用BINARY(16)来存储UUID,也就16字节,并且MySQL8+还内置了一些函数做针对性的处理(UUID_TO_BIN、BIN_TO_UUID 和 IS_UUID)。虽然还是比不过bigint,比CHAR(36)要好太多了。
当然既然有了时间戳,就避免不了面对时钟回拨问题,但正如前面说的,UUID V7有74位(12位随机数A+62位随机数B)随机数,在毫秒内碰撞的概率极低,数学原理和公式我就不列了,要做到50%的碰撞概率,1毫秒内要生成5300亿个ID。。。。我觉得,绝大多数情况下,我们用不着杞人忧天吧。
数据库
建表:
-- 使用 BINARY(16)
CREATE TABLE users (
id BINARY(16) PRIMARY KEY,
name VARCHAR(100)
);
新增:
-- 这两个ID就是我使用程序生成的UUID V7
INSERT INTO users (id, name) VALUES (uuid_to_bin('0198cb7e-5126-7813-b7c2-a5f166b1c76a'), 'Bone');
INSERT INTO users (id, name) VALUES (uuid_to_bin('0198cb41-12bd-7d37-a5d3-872eac14847c'), 'Leo');
注意,我是先新增Bone,后新增Leo,而Leo的UUID,生成时间早于Bone的UUID。
因为是二进制,所以直接查询,大概是这个样子:
要想可读性高一点,可以:
select bin_to_uuid(id),name from users;
按照ID排序也是没问题的:
select bin_to_uuid(id),name from users order by id desc;
因为Bone的UUID生成时间晚于Leo,所以倒序排,即便是Bone先新增入库,Bone也是在前面。
另外,因为UUID V7的时间戳可以天然地作为创建时间,所以我们可以这么创建表:
CREATE TABLE users (
id BINARY(16) PRIMARY KEY,
name VARCHAR(100),
create_time_ms BIGINT UNSIGNED GENERATED ALWAYS AS (CONV(HEX(SUBSTR(id, 1, 6)), 16, 10)) STORED
);
新增记录的时候,直接插入UUID V7,时间戳也自动生成了。不过从工作实践中看,绝大多数情况下,还是使用独立的时间戳:
CREATE TABLE users2 (
id BINARY(16) PRIMARY KEY,
name VARCHAR(100),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
这样我们就要保证程序生成UUID V7的时间与新增到数据库的时间别差太多就行。
代码
现在JDK应该还没有官方支持UUID V7,需要第三方库,例如uuid-creator:
<dependency>
<groupId>com.github.f4b6a3</groupId>
<artifactId>uuid-creator</artifactId>
<version>6.1.1</version>
</dependency>
Java代码:
public class Test {
public static void main(String[] args) {
Test test = new Test();
test.testUC();
}
public void testUC() {
UUID uuid = UuidCreator.getTimeOrderedEpoch();
System.out.println("uc id1 :" + uuid);
System.out.println("当前时间 :" + new Date().toInstant());
System.out.println("uuid的时间 :" + UuidUtil.getInstant(uuid));
System.out.println("uuid的时间(秒) :" + UuidUtil.getInstant(uuid).getEpochSecond());
System.out.println("uuid的时间:(毫秒):" + UuidUtil.getInstant(uuid).toEpochMilli());
System.out.println("===============");
//也可以这么生成
GUID guid = GUID.v7();
UUID uuid2 = guid.toUUID();
String uuid2Str = guid.toString();
System.out.println("uc id2 :" + uuid2);
System.out.println("uc id2 字符串:" + uuid2Str);
System.out.println("uuid2的时间 :" + UuidUtil.getInstant(uuid2));
}
}