由 John Doe 五月 19, 2024
摘要:在本文中,我们将了解 PL/pgSQL 异常和子事务之间的关系。
目录
介绍
PL/pgSQL 中的BEGIN / EXCEPTION WHEN .. / END
块是用子事务实现的,因此 PL/pgSQL 函数中的EXCEPTION
块会递增事务 ID(xid)。
为什么 PL/pgSQL 需要子事务(或保存点)来处理异常?
好吧,如果你考虑一下当你捕获一个异常时,你可能想恢复你的执行,也就是说,你必须有一种方法来回滚你的工作单元并重新开始。
测试准备工作
可以使用一个简单的函数和一个要滥用子事务的表,来检查运行中的事务。让我们看看函数:
CREATE OR REPLACE FUNCTION f_loop(b int DEFAULT 0, e int DEFAULT 10)
RETURNS void AS $$
BEGIN
RAISE DEBUG 'TXID of the function (here should not be assigned) function: % %',
txid_current_if_assigned(),
txid_status(txid_current_if_assigned());
FOR f IN b .. e LOOP
BEGIN
RAISE DEBUG 'Before INSERT of % TXID: % SNAPSHOT: %',
f,
txid_current_if_assigned(),
txid_current_snapshot();
INSERT INTO foo( i ) VALUES( f );
RAISE DEBUG 'After INSERT of % TXID: % SNAPSHOT: %',
f,
txid_current_if_assigned(),
txid_current_snapshot();
EXCEPTION
WHEN UNIQUE_VIOLATION THEN
RAISE DEBUG 'Exception for % TXID: % SNAPSHOT: %',
f,
txid_current_if_assigned(),
txid_current_snapshot();
END;
END LOOP;
END;
$$ LANGUAGE plpgsql;
该函数接受一个开始和结束的序号,并循环遍历它们之间的每个值,试图将值插入表中。在每一步,包括异常块中,我们会调用txid_current_if_assigned()
,以报告事务 ID(xid
),并调用txid_current_snapshot()
,以提供当前快照,这大致意味着这个事务正在“经历”的最小和最大 xid。
表的定义非常简单:它有一个列,列上有一个UNIQUE
约束,这也是函数将会违反的约束。
CREATE TABLE foo (i int PRIMARY KEY);
第一次运行:无异常
由于表为空,因此插入从1
到10
的值不会产生任何异常。
testdb=> SELECT f_loop( 1, 10 );
DEBUG: TXID of the function (here should not be assigned) function: <NULL> <NULL>
DEBUG: Before INSERT of 1 TXID: <NULL> SNAPSHOT: 4748:4748:
DEBUG: After INSERT of 1 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: Before INSERT of 2 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: After INSERT of 2 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: Before INSERT of 3 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: After INSERT of 3 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: Before INSERT of 4 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: After INSERT of 4 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: Before INSERT of 5 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: After INSERT of 5 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: Before INSERT of 6 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: After INSERT of 6 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: Before INSERT of 7 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: After INSERT of 7 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: Before INSERT of 8 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: After INSERT of 8 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: Before INSERT of 9 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: After INSERT of 9 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: Before INSERT of 10 TXID: 4748 SNAPSHOT: 4748:4748:
DEBUG: After INSERT of 10 TXID: 4748 SNAPSHOT: 4748:4748:
f_loop
--------
(1 row)
在第一轮循环时,xid
是NULL
,因为该函数(尚未)修改任何内容。这就是为什么会使用txid_current_if_assigned()
而不是txid_current()
,以避免浪费一个事务 ID。一旦函数开始修改数据(即,在第一个INSERT
之后),事务会从虚拟提升到具体,因此会分配xid
。由于根本没有引发异常,因此函数的xid
和快照都是固定的。
第二次运行:一半异常
让我们在有一些数字重叠的情况下运行它,以便成功插入一半的值,而另一半会抛出异常。
testdb=> SELECT f_loop( 5, 15 );
DEBUG: TXID of the function (here should not be assigned) function: <NULL> <NULL>
DEBUG: Before INSERT of 5 TXID: <NULL> SNAPSHOT: 4760:4760:
DEBUG: Exception for 5 TXID: 4760 SNAPSHOT: 4760:4762:
DEBUG: Before INSERT of 6 TXID: 4760 SNAPSHOT: 4760:4762:
DEBUG: Exception for 6 TXID: 4760 SNAPSHOT: 4760:4763:
DEBUG: Before INSERT of 7 TXID: 4760 SNAPSHOT: 4760:4763:
DEBUG: Exception for 7 TXID: 4760 SNAPSHOT: 4760:4764:
DEBUG: Before INSERT of 8 TXID: 4760 SNAPSHOT: 4760:4764:
DEBUG: Exception for 8 TXID: 4760 SNAPSHOT: 4760:4765:
DEBUG: Before INSERT of 9 TXID: 4760 SNAPSHOT: 4760:4765:
DEBUG: Exception for 9 TXID: 4760 SNAPSHOT: 4760:4766:
DEBUG: Before INSERT of 10 TXID: 4760 SNAPSHOT: 4760:4766:
DEBUG: Exception for 10 TXID: 4760 SNAPSHOT: 4760:4767:
DEBUG: Before INSERT of 11 TXID: 4760 SNAPSHOT: 4760:4767:
DEBUG: After INSERT of 11 TXID: 4760 SNAPSHOT: 4760:4767:
DEBUG: Before INSERT of 12 TXID: 4760 SNAPSHOT: 4760:4767:
DEBUG: After INSERT of 12 TXID: 4760 SNAPSHOT: 4760:4767:
DEBUG: Before INSERT of 13 TXID: 4760 SNAPSHOT: 4760:4767:
DEBUG: After INSERT of 13 TXID: 4760 SNAPSHOT: 4760:4767:
DEBUG: Before INSERT of 14 TXID: 4760 SNAPSHOT: 4760:4767:
DEBUG: After INSERT of 14 TXID: 4760 SNAPSHOT: 4760:4767:
DEBUG: Before INSERT of 15 TXID: 4760 SNAPSHOT: 4760:4767:
DEBUG: After INSERT of 15 TXID: 4760 SNAPSHOT: 4760:4767:
f_loop
--------
(1 row)
正如你所看到的,在前五个数字中都有报告异常。函数的xid
保持不变,但快照增加了 6 个事务标识符(一个用于函数,5 个用于子事务)。在此之后,其余五个值将成功插入,因此快照不再增长。
这些子事务在哪里?
如果您现在检查表的 MVCC 值,您可以看到插入的每个值都具有不同的事务 ID xmin
,而不用考虑函数调用是否捕获过异常。
SELECT xmin, xmax, cmin, cmax, * FROM foo;
xmin | xmax | cmin | cmax | i
------|------|------|------|----
4749 | 0 | 0 | 0 | 1
4750 | 0 | 1 | 1 | 2
4751 | 0 | 2 | 2 | 3
4752 | 0 | 3 | 3 | 4
4753 | 0 | 4 | 4 | 5
4754 | 0 | 5 | 5 | 6
4755 | 0 | 6 | 6 | 7
4756 | 0 | 7 | 7 | 8
4757 | 0 | 8 | 8 | 9
4758 | 0 | 9 | 9 | 10
4767 | 0 | 6 | 6 | 11
4768 | 0 | 7 | 7 | 12
4769 | 0 | 8 | 8 | 13
4770 | 0 | 9 | 9 | 14
4771 | 0 | 10 | 10 | 15
(15 rows)
如何模拟相同的行为
保存点的行为几乎相同!因此,让我们截断表并在其中插入新值,其中包含一个显式事务和多个保存点:
TRUNCATE foo;
BEGIN;
INSERT INTO foo(i) VALUES(1);
SAVEPOINT S1;
INSERT INTO foo(i) VALUES(2);
SAVEPOINT S2;
INSERT INTO foo(i) VALUES(3);
SAVEPOINT S3;
COMMIT;
SELECT xmin,xmax, cmin, cmax, * FROM foo;
xmin | xmax | cmin | cmax | i
------|------|------|------|---
4779 | 0 | 0 | 0 | 1
4780 | 0 | 1 | 1 | 2
4781 | 0 | 2 | 2 | 3
(3 rows)
正如你所看到的,每执行一个INSERT
就会连续递增一次xmin
。
结论
在 PL/pgSQL(可能也在其他语言中)中,很确定异常是通过子事务来实现的。因此,您需要注意那些带有异常块的 PL/pgSQL 函数或触发器。