顾乔芝士网

持续更新的前后端开发技术栈

推荐大家试用一下UUID V7做数据库主键

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));
    }
}
控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言