Plaza 新闻汇总

PostgreSQL UUIDv7 及其每个后端单调性

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 年底发布)之前不会进入正式版本。所以现在,我们等待。

原文地址
2025-01-02 21:20:50