由 John Doe 十二月 13, 2024
摘要:在本文中,我们将了解 PostgreSQL 事务 ID 快速分配带来的数据冻结问题,以及相关的解决方案。
目录
事务号分配场景: 用例
下面我们在 PostgreSQL 的数据库中,创建一个表和一些用于数据操作的函数。
CREATE TABLE t_large (id integer, name text);
CREATE UNIQUE INDEX large_idx ON t_large (id);
CREATE OR REPLACE FUNCTION f_base(v int)
RETURNS void AS $$
BEGIN
INSERT INTO t_large (id, name) VALUES(v, 'xxx');
EXCEPTION
WHEN UNIQUE_VIOLATION THEN
RAISE WARNING 'Exception for value: %', v;
END; $$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION f_loop(b int, e int)
RETURNS void AS $$
BEGIN
FOR i IN b .. e LOOP
PERFORM f_base(i);
END LOOP;
END; $$ LANGUAGE plpgsql;
让我们通过存储函数插入一些数据,查看操作能否执行成功,在各个子事务中的事务 ID 是否会发生变化。
SELECT f_loop(1, 4);
SELECT ctid, xmin, * FROM t_large;
ctid | xmin | id | name
-------+------+----+------
(0,1) | 743 | 1 | xxx
(0,2) | 744 | 2 | xxx
(0,3) | 745 | 3 | xxx
(0,4) | 746 | 4 | xxx
(4 rows)
事务号分配场景: 内部原理
下面是我们在 PostgreSQL 中运行上面的存储函数时,数据库系统全局事务 ID 发生的变化:
- PostgreSQL 使用了子事务,来实现存储函数中的异常处理代码块。
- 每个事务/子事务都有一个事务 ID,它有点像数据库系统的运行时间。
- 每行都有两个默认的隐藏属性 xmin 和 xmax,这两个属性分别保存了创建和更改它们的事务/子事务的 ID。
- 系统中最多出现的事务 ID 数目约为 20 亿,在超过该数目后,MVCC 的机制将无法正常工作。
事务号分配场景: 思考
问题影响
- 需要定期扫描所有数据行,进行事务 ID 的冻结操作。
- 如果再叠加长事务的发生,会出现事务 ID 耗尽,数据库停止服务。
- 容易出现 pg_xact、pg_subtrans 缓存访问的等待事件,系统整体性能下降。
应对方案
- 配置监控告警,检查事务 ID 耗尽
- 配置监控告警,检查子事务的使用情况
- 配置监控告警,检查长时间运行的事务
Redrock Postgres 的解决方案
Redrock Postgres 是基于撤消日志记录位置实现的子事务,这些子事务不过是事务执行过程中的一个临时标记,它们并不需要分配独立的事务 ID,不会对系统造成任何风险。
另外,在 Redrock Postgres 中,事务 ID 类型xid
是 8 字节的长度,可以近乎无限使用,无需担心事务 ID 用尽的风险。
下面我们在 Redrock Postgres 的数据库中,创建和上面相同的表和相同的操作函数,通过存储函数插入同样的数据量,查看事务 ID 在各个子事务中是否会发生变化。
SELECT f_loop(1, 4);
SELECT ctid, rowxid, * FROM t_large;
ctid | rowxid | id | name
-------+-------------+----+------
(0,1) | (8,64,4988) | 1 | xxx
(0,2) | (8,64,4988) | 2 | xxx
(0,3) | (8,64,4988) | 3 | xxx
(0,4) | (8,64,4988) | 4 | xxx
(4 rows)
从上面的测试结果来看,在我们对表t_large
进行数据插入完后,数据库系统只消耗了一个全局事务 ID。子事务使用了和所属事务块同样的事务 ID,它们并不需要分配独立的事务 ID。