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 中,事务的设置是通过使用 BEGIN 和 COMMIT 命令包围事务的 SQL 命令来完成的。因此,我们的银行事务实际上看起来像:
BEGIN;
UPDATE accounts SET balance = balance - 100.00
WHERE name = 'Alice';
-- etc etc
COMMIT;
如果在事务过程中,我们决定不提交(也许我们刚刚注意到 Alice 的余额为负),我们可以发出 ROLLBACK 命令而不是 COMMIT,我们到目前为止的所有更新都将被取消。
实际上,PostgreSQL 将每个 SQL 语句视为在一个事务中执行。如果您没有发出 BEGIN 命令,则每个单独的语句都有一个隐式的 BEGIN 和(如果成功) COMMIT 包裹在它周围。有时会将由 BEGIN 和 COMMIT 包围的一组语句称为 transaction block。
Note
一些客户端库会自动发出 BEGIN 和 COMMIT 命令,因此您可能无需询问即可获得事务块的作用。查阅所用接口的文档。
可以通过使用 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 是重新控制被系统由于错误而置于已中止状态的事务块的唯一方法。