3.4 事务#
事务是所有数据库系统的一个基本概念。事务的要点是将多个步骤捆绑到一个单一的、全有或全无的操作中。步骤之间的中间状态对其他并发事务不可见,如果发生某些故障阻止事务完成,那么没有一个步骤会影响数据库。
例如,考虑一个银行数据库,其中包含各个客户账户的余额,以及分行的总存款余额。假设我们要记录从爱丽丝账户到鲍勃账户的 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');
这些命令的详细信息在此并不重要;重要的是,完成此相当简单的操作涉及多个单独的更新。我们银行的官员希望确保所有这些更新都发生,或者没有一个发生。系统故障导致鲍勃收到未从爱丽丝借记的 100.00 美元肯定不行。如果鲍勃没有得到信用,爱丽丝也不会再是一个快乐的客户。我们需要保证,如果操作中途出现问题,则到目前为止执行的任何步骤都不会生效。将更新分组到一个事务中可以为我们提供此保证。事务被称为原子:从其他事务的角度来看,它要么完全发生,要么根本不发生。
我们还希望保证,一旦事务完成并由数据库系统确认,它就已经被永久记录,并且即使此后不久发生崩溃,也不会丢失。例如,如果我们正在记录鲍勃的现金取款,我们不希望在他走出银行大门后,他的账户借记会在崩溃中消失。事务数据库保证,在事务报告完成之前,事务所做的所有更新都将记录在永久存储中(即磁盘上)。
事务数据库的另一个重要特性与原子更新的概念密切相关:当多个事务同时运行时,每个事务都不应该能够看到其他事务所做的不完整更改。例如,如果一个事务忙于计算所有分支余额,那么它不应包括爱丽丝分支的借记而不包括鲍勃分支的贷记,反之亦然。因此,事务必须是全有或全无的,不仅在它们对数据库的永久影响方面,而且在它们发生时的可见性方面也是如此。在事务完成之前,由打开的事务进行的更新对其他事务是不可见的,而在事务完成后,所有更新将同时变为可见。
在PostgreSQL中,通过用BEGIN
和COMMIT
命令包围事务的 SQL 命令来设置事务。因此,我们的银行事务实际上看起来像
BEGIN;
UPDATE accounts SET balance = balance - 100.00
WHERE name = 'Alice';
-- etc etc
COMMIT;
如果在事务进行过程中,我们决定不提交(也许我们刚刚注意到爱丽丝的余额变为负数),我们可以发出ROLLBACK
命令而不是COMMIT
,并且我们迄今为止的所有更新都将被取消。
PostgreSQL实际上将每个 SQL 语句视为在事务中执行。如果您不发出BEGIN
命令,则每个单独的语句都有一个隐式的BEGIN
和(如果成功)COMMIT
包含在其中。由BEGIN
和COMMIT
包围的一组语句有时称为事务块。
注意
一些客户端库自动发出BEGIN
和COMMIT
命令,以便您可以在不询问的情况下获得事务块的效果。检查您正在使用的界面的文档。
可以通过使用保存点以更精细的方式控制事务中的语句。保存点允许您有选择地丢弃事务的部分,同时提交其余部分。在使用SAVEPOINT
定义保存点后,如果需要,您可以使用ROLLBACK TO
回滚到保存点。在定义保存点和回滚到保存点之间的事务的所有数据库更改都将被丢弃,但早于保存点的更改将被保留。
回滚到保存点后,它仍然被定义,因此你可以多次回滚到它。相反,如果你确定不再需要回滚到特定保存点,则可以释放它,以便系统可以释放一些资源。请记住,释放或回滚到保存点都会自动释放之后定义的所有保存点。
所有这些都在事务块内发生,因此其他数据库会话都看不到它。当你提交事务块时(如果提交),提交的操作将作为一个单元对其他会话可见,而回滚的操作将永远不会可见。
记住银行数据库,假设我们从 Alice 的账户中借记 100.00 美元,并贷记 Bob 的账户,后来才发现我们应该贷记 Wally 的账户。我们可以使用保存点像这样来实现
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
是重新控制因错误而被系统置于中止状态的事务块的唯一方法,除了完全回滚并重新开始。