3.4. Transactions

事务是所有数据库系统的基本概念。事务的本质是将多个步骤 Binding 成一个单一的“全有或全无”操作。步骤之间的中间状态对于其他并发事务是不可见的,并且如果发生某些故障导致该事务无法完成,则所有步骤都不会影响数据库。

例如,考虑一个银行数据库,其中包含各种 Client 帐户的余额以及分支机构的总存款余额。假设我们要记录从 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');

这些命令的细节在这里并不重要;重要的一点是,要完成此相当简单的操作,需要涉及几个单独的更新。我们银行的 Managers 将希望确保所有这些更新都发生了,或者没有发生。当然,如果系统故障导致 Bob 收到未从 Alice 借记的$ 100.00,也不会这样做。如果在没有 Bob 的信用的情况下将其借记,Alice 也永远不会成为一个满意的 Client。我们需要保证,如果在操作过程中出现问题,到目前为止执行的所有步骤都不会生效。将更新分组为一个“事务”为我们提供了这一保证。一笔 Transaction 被称为“原子”Transaction:从其他 Transaction 的角度来看,它要么完全发生,要么根本不发生。

我们还希望保证,一旦事务完成并由数据库系统确认,它的确已被永久记录,即使之后不久发生崩溃也不会丢失。例如,如果我们正在记录 Bob 提取的现金,我们不希望他的帐户中的借方在他走出银行大门后的崩溃中消失的任何可能性。事务性数据库保证在报告事务完成之前,事务所做的所有更新都记录在永久性存储中(即,在磁盘上)。

事务数据库的另一个重要属性与原子更新的概念密切相关:当多个事务同时运行时,每个事务都应该看不到其他事务所做的不完整更改。例如,如果一笔 Transaction 正忙于汇总所有分支机构的余额,那么它就不会包括从 Alice 的分支机构借来的款项,而不包括 Bob 分支机构的贷项,反之亦然。因此,Transaction 不仅必须是对数据库的永久影响,而且还必须视它们发生时的可见性而定。迄今为止,一个打开的事务所做的更新对于其他事务是不可见的,直到该事务完成为止,所有更新同时变为可见。

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

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

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

PostgreSQL 实际上将每个 SQL 语句都视为在事务内执行。如果您不发出BEGIN命令,那么每个单独的语句都将包含一个隐含的BEGIN和(如果成功的话)COMMIT。由BEGINCOMMIT包围的一组语句有时称为事务块

Note

一些 Client 端库会自动发出BEGINCOMMIT命令,以便您无需询问即可获得事务处理块的效果。查看所用接口的文档。

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

回滚到保存点后,将 continue 对其进行定义,因此您可以回滚几次。相反,如果您确定不需要再次回滚到特定的保存点,则可以将其释放,因此系统可以释放一些资源。请记住,释放或回滚到保存点将自动释放其后定义的所有保存点。

所有这些都是在事务块内发生的,因此其他数据库会话都看不到它。当并且如果您提交事务块,则已提交的操作作为一个单元在其他会话中变得可见,而回滚的操作则永远不会变得可见。

记住银行数据库,假设我们从爱丽丝的帐户中借了 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是重新获得对由于错误而被系统置于异常中止状态的事务块的控制的唯一方法,因为它无法完全回滚并重新启动。