PostgreSQL 本月早些时候提交了 UUIDv7 的实现。这些 UUID 拥有 UUIDv4(随机)的所有优点,但使用当前时间生成更具确定性的顺序,并且在使用 B 树等有序结构进行插入时性能更好。
一个令人惊喜的特点是,UUID 的随机部分在每个 PostgreSQL 后端中都是单调的:
在我们的实现中,12 位亚毫秒时间戳分数存储在时间戳之后,在 RFC 中称为“rand_a”的空间中。这确保了在毫秒内额外的单调性。rand_a 位也充当计数器。我们选择一个亚毫秒时间戳,以便在同一后端生成的 UUID 单调递增,即使系统时钟倒退或以非常高的频率生成 UUID 也是如此。因此,生成的 UUID 的单调性在同一后端内得到保证。
这在实践中是一个非常有价值的功能,尤其是在测试中。假设您想要为测试 API 列表端点生成五个对象。它们有可能因为跨越不同的毫秒或碰巧而按顺序生成,但概率对您不利,并且可能性是一些将是无序的。测试用例必须生成五个对象,然后在使用它们之前进行初始排序。这并不是世界末日,但它需要更多的测试代码并增加了噪音。
```
test_accounts = 5.times.map { TestFactory.account }
# 可能 ID 按顺序排列,也可能不是,所以进行初始排序
test_accounts.sort_by! { |a| a.id }
# API 端点将返回按 ID 排序的账户
resp = make_api_request :get, "/accounts"
expect(resp.map { _1["id"] }).to eq(test_accounts.map(&:id))
```
通过 PostgreSQL 确保 UUIDv7 的单调性,五个生成的个体获得五个按顺序排列的 ID,使测试更安全¹并且编写速度更快。跨后端的单调性没有保证,但在编写良好的测试套件中是可以的。像测试事务这样的模式将保证每个测试用例都只与一个后端通信。
12 位更多时钟
我对单调性的理解一直很差,所以我很好奇它是如何在这里实现的。我查看了补丁,它的方法比我预期的更明显:
```
/*
* 根据 RFC 9562 生成 UUID 版本 7,使用给定的时间戳。
*
* UUID 版本 7 包含一个以毫秒为单位的 Unix 时间戳(48
* 位)和 74 个随机位,不包括所需的版本和
* 变体位。为了确保在高频率 UUID 生成的情况下单调性,
* 我们采用“用增加的时钟精度替换最左边的随机位(方法 3)”
* 的方法,如 RFC 中所述。此方法利用来自
* “rand_a”位的 12 位来存储亚毫秒精度的 1/4096(或 2^12)分数。
*
* ns 是自 UNIX 纪元开始以来的纳秒数。
* 此值用于 UUID 的时间相关位。
*/
static pg_uuid_t* generate_uuidv7(int64 ns) {
...
/*
* 亚毫秒时间戳分数(SUBMS_BITS 位,而不是
* SUBMS_MINIMAL_STEP_BITS)
*/
increased_clock_precision = ((ns % NS_PER_MS) * (1 << SUBMS_BITS)) / NS_PER_MS;
/* 将增加的时钟精度填充到“rand_a”位 */
uuid->data[6] = (unsigned char) (increased_clock_precision >> 8);
uuid->data[7] = (unsigned char) (increased_clock_precision);
/* 用随机字节填充增加的时钟精度之后的所有内容 */
if (!pg_strong_random(&uuid->data[8], UUID_LEN - 8))
ereport(ERROR,
(errcode(ERRCODE_INTERNAL_ERROR),
errmsg("could not generate random values")));
```
UUIDv7 指定一个初始的 48 位,它对时间戳进行编码,精度达到毫秒级。对于人类来说,毫秒是一段很短的时间,但对于计算机来说却很长,并且可以很容易地在单毫秒的空间内生成许多 UUID。
```
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 48 bits unix_ts_ms |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 48 bits unix_ts_ms (cont) | ver | 12 bits rand_a |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| 62 bits rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 62 bits rand_b (cont) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
PostgreSQL 补丁通过重新利用 UUID 随机组件的 12 位来解决此问题,从而将时间戳的精度提高到纳秒级(填充上面的 rand_a),在实践中,这种精度太高,无法包含在同一进程中生成的两个 UUIDv7。它使进程之间重复 UUID 的可能性更高,但仍然有 62 位随机性可以使用,因此冲突仍然极不可能。
等待
UUIDv7 将成为 PostgreSQL 的一个很棒的核心补充,我迫不及待地想开始使用它们。非常不幸的是,它们的提交被推迟到 PostgreSQL 17 的冻结期之后,所以它们在 PostgreSQL 18(将于 2025 年底发布)之前不会进入正式版本。所以现在,我们等待。