当我们选择数据库的主键类型时,通常会考虑自增数字或者 UUID。但是这两种类型都有优缺点,自增字段简单有序,性能良好,最大的问题是无法保证全局唯一性,分布式场景受限;UUID 具有全局唯一性,适合分布式应用,但是早期版本的 UUID 具有随机性,性能不如自增数字。
不过,随着 UUIDv7 的出现,已经解决了 UUID 没有随着时间递增的问题,它的组成如下:
UUIDv7 包含 128 比特(每 4 比特组成一个十六进制数字),具体包含:
- 48 比特时间戳(精度为毫秒);
- 4 比特UUID 版本(7);
- 12 比特随机数字;
- 2 比特(UUID 类型);
- 62 比特随机数字。
完整的 UUIDv7 介绍可以参考 RFC 9562。
UUIDv7 最大的优势是具有时间递增性和全局唯一性,非常适合作为数据库的主键,包括分布式数据库。
很多编程语言都提供了生成 UUIDv7 的代码库,不过今天我们要介绍的是在数据库中使用 SQL 实现 UUIDv7,完全不需要依赖其他组件。
首先,我们来看一下 PostgreSQL 中的实现:
select
-- timestamp
lpad(to_hex(((extract(epoch from now()) * 1000)::bigint >> 16)), 8, '0') || '-' ||
lpad(to_hex(((extract(epoch from now()) * 1000
+ (date_part('milliseconds', now())::bigint % 1000))::bigint & 0xffff)), 4, '0') || '-' ||
-- version
lpad(to_hex((0x7000 + (random() * 0x0fff)::int)), 4, '0') || '-' ||
-- variant
lpad(to_hex((0x8000 + (random() * 0x3fff)::int)), 4, '0') || '-' ||
-- randomness
lpad(to_hex((floor(random() * (2^48))::bigint >> 16)), 12, '0') AS uuid7;
uuid7 |
------------------------------------+
01904fcb-0ee8-7d9c-a192-000052989c99|
把上面的查询定义为一个函数,就可以实现代码复用了。
接下来是 SQLite 中的实现:
select
-- timestamp
format('%08x', ((strftime('%s') * 1000) >> 16)) || '-' ||
format('%04x', ((strftime('%s') * 1000)
+ ((strftime('%f') * 1000) % 1000)) & 0xffff) || '-' ||
-- version
format('%04x', 0x7000 + abs(random()) % 0x0fff) || '-' ||
-- variant
format('%04x', 0x8000 + abs(random()) % 0x3fff) || '-' ||
-- randomness
format('%012x', abs(random()) >> 16) as value;
value |
------------------------------------+
01904fd0-3dae-7460-8d4c-7d0c6b483299|
SQlite 没有自定义函数,可以把上面的查询定义为一个视图,实现代码复用。
其他数据库也可以按照相同的思路实现,欢迎补充。