PL/pgSQL 异常和子事务

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);

第一次运行:无异常

由于表为空,因此插入从110的值不会产生任何异常。

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)

在第一轮循环时,xidNULL ,因为该函数(尚未)修改任何内容。这就是为什么会使用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 函数或触发器。