Postgres UUIDv7 + 每个后端单调性


本月初, Postgres 已承诺实施UUIDv7 。它具有 v4(随机)UUID 的所有优点,但使用当前时间以更确定的顺序生成,并且在使用 B 树等有序结构进行插入时性能明显更好。

令人惊喜的是,UUID 的随机部分在每个 Postgres 后端内都是单调的:

在我们的实现中,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))

Postgres 确保 UUIDv7 的单调性,生成的五个对象将获得五个按顺序排列的 ID,从而使测试更安全1且编写速度更快。单调性无法跨后端保证,但在编写良好的测试套件中这是可以接受的。测试事务等模式将保证每个测试用例只与一个后端对话

时钟频率增加 12 位
我对单调性的理解总是很模糊,所以我很好奇它是如何在这里实现的。我查看了补丁,它的方法比我预期的更明显:

/*
 * Generate UUID version 7 per RFC 9562, with the given timestamp.
 *
 * UUID 第 7 版包括以毫秒为单位的 Unix 时间戳(48 * 位)和 74 个随机位,不包括所需的版本和 * 变体位。
 *为了确保高频率生成 UUID 时的单调性,我们采用了 RFC 中描述的 "用更高的时钟精度替换最左边的随机位(方法 3)"。
  *该方法利用 "rand_a "位中的 12 位来存储 1/4096(或 2^12)分 * 毫秒精度。
 */

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;

/* Fill the increased clock precision to "rand_a" bits */
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)                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Postgres 补丁通过重新利用 UUID 随机分量的 12 位来解决该问题,将时间戳的精度提高到纳秒级(填充上文),实际上这太精确了,无法包含同一进程中生成的两个 UUIDv7。这使得进程之间rand_a出现重复的 UUID 的可能性更大,但仍然有 62 位随机性可以利用,因此发生冲突的可能性仍然很小。

等待正在进行
UUIDv7 将成为 Postgres 的一项重要核心补充,我迫不及待地想要开始使用它们。不幸的是,它们的提交被推迟到了 Postgres 17 冻结之后,因此直到 2025 年底 Postgres 18 完成之前,它们都不会成为正式版本。所以现在,我们拭目以待。