Postgresql 中文操作指南

3.4. Transactions #

Transactions 是所有数据库系统的一个基本概念。事务的关键点是它将多个步骤捆绑到一个所有或无,要么全部要么全不的,操作中。步骤之间的中间状态对其他并发事务不可见,如果发生某些故障阻止事务完成,则没有任何步骤会影响数据库。

例如,考虑一个银行数据库,其中包含各个客户帐户的余额,以及各支行的总存款余额。假设我们要记录从 Alice 的帐户向 Bob 的帐户支付 100.00 美元的款项。简单起见,用于此操作的 SQL 命令可能如下:

UPDATE accounts SET balance = balance - 100.00
    WHERE name = 'Alice';
UPDATE branches SET balance = balance - 100.00
    WHERE name = (SELECT branch_name FROM accounts WHERE name = 'Alice');
UPDATE accounts SET balance = balance + 100.00
    WHERE name = 'Bob';
UPDATE branches SET balance = balance + 100.00
    WHERE name = (SELECT branch_name FROM accounts WHERE name = 'Bob');

这些命令的详细信息在此并不重要;重点在于完成此相当简单的操作涉及几个单独的更新。我们银行的官员希望确保所有这些更新都会发生,或都不会发生。系统故障导致 Bob 收到一笔未从 Alice 借记的 100.00 美元肯定是不行的。如果在 Bob 未记入贷项的情况下 Alice 被借记,她也不会长时间保持愉快的客户关系。我们需要保证,如果操作中途出现问题,到目前为止执行的任何步骤都不会生效。将更新分组到 transaction 可以为我们提供此保证。据称事务是 atomic:从其他事务的角度来看,它要么完全发生,要么根本不发生。

我们还需要保证,一旦事务完成并得到数据库系统的确认,它确实已经永久记录,并且即使很快发生崩溃也不会丢失。例如,如果我们正在记录 Bob 提取现金,我们不希望他刚走出银行大门,银行账户的借项就因崩溃而消失。事务型数据库保证在报告事务完成之前,事务所做的所有更新都记录在永久存储(即磁盘上)。

事务数据库的另一个重要属性与原子更新的概念密切相关:当多个事务同时运行时,每个事务不应该能够看到其他人所做的不完整更改。例如,如果一个事务忙于统计所有支行的余额,那么只包含 Alice 所在支行的借项而不包含转入 Bob 所在支行的贷项是不行的,反之亦然。因此,事务不仅在它们对数据库的永久影响方面必须要么全部要么全不,而且在它们发生时以可见性方面也必须如此。由开放事务到目前为止所做的更新对其他事务是不可见的,直至事务完成,然后所有更新同时变得可见。

在 PostgreSQL 中,事务的设置是通过使用 BEGINCOMMIT 命令包围事务的 SQL 命令来完成的。因此,我们的银行事务实际上看起来像:

BEGIN;
UPDATE accounts SET balance = balance - 100.00
    WHERE name = 'Alice';
-- etc etc
COMMIT;

如果在事务过程中,我们决定不提交(也许我们刚刚注意到 Alice 的余额为负),我们可以发出 ROLLBACK 命令而不是 COMMIT,我们到目前为止的所有更新都将被取消。

实际上,PostgreSQL 将每个 SQL 语句视为在一个事务中执行。如果您没有发出 BEGIN 命令,则每个单独的语句都有一个隐式的 BEGIN 和(如果成功) COMMIT 包裹在它周围。有时会将由 BEGINCOMMIT 包围的一组语句称为 transaction block

Note

一些客户端库会自动发出 BEGINCOMMIT 命令,因此您可能无需询问即可获得事务块的作用。查阅所用接口的文档。

可以通过使用 savepoints 以更加精细的方式控制事务中的语句。保存点允许您有选择性地取消事务的一部分,同时提交其余部分。在使用 SAVEPOINT 定义保存点后,如果需要,您可以使用 ROLLBACK TO 回滚到保存点。在定义保存点和回滚到它之间的所有事务数据库更改将被取消,但早于保存点的更改将被保留。

回滚到保存点后,此保存点将继续被定义,因此可以回滚到此保存点多次。相反,如果您确定您不再需要回滚到某个特定保存点,则可以将其释放,以便系统可以释放一些资源。请记住,释放或回滚到保存点都会自动释放之后定义的所有保存点。

所有这些都在事务块内发生,所以其他数据库会话都看不到这些内容。如果您提交事务块(如果提交),已提交的动作将作为一个单元被其他会话看到,而回滚的动作永远不会被看到。

记住银行数据库,假设我们从艾丽丝的账户借记 100.00 美元,并将钱存入鲍勃的账户,结果后来发现我们应该将钱存入沃利的账户。我们可以像这样使用保存点来实现:

BEGIN;
UPDATE accounts SET balance = balance - 100.00
    WHERE name = 'Alice';
SAVEPOINT my_savepoint;
UPDATE accounts SET balance = balance + 100.00
    WHERE name = 'Bob';
-- oops ... forget that and use Wally's account
ROLLBACK TO my_savepoint;
UPDATE accounts SET balance = balance + 100.00
    WHERE name = 'Wally';
COMMIT;

当然,这个例子过于简单化,但使用保存点可以在事务块中实现大量控制。此外,除了完全回滚并重新开始外,ROLLBACK TO 是重新控制被系统由于错误而置于已中止状态的事务块的唯一方法。