事务是所有数据库系统的一个基本概念。事务的要点是将多个步骤捆绑到一个全部或无的单个操作中。步骤之间的中间状态不会对其他并发事务可见,如果发生某些故障导致事务无法完成,那么在数据库中 هیچ一个步骤都会产生影响。
例如,考虑包含各种客户账户余额以及各个支行的总存款余额的银行数据库。假设我们想要记录从爱丽丝账户支付给鲍勃账户的 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 美元的情况,肯定不行。如果没有给鲍勃记账,爱丽丝也不会这么快成为快乐的客户。我们需要保证,如果操作过程中出现问题,那么到目前为止执行的任何步骤都不会生效。将更新分组到事务中可以给我们此担保。事务被称为原子性:从其他事务的角度来看,它要么完全发生,要么根本不发生。
我们还需要保证,一旦事务完成并得到数据库系统的确认,它就已经被永久记录,即使随后不久发生故障也不会丢失。例如,如果我们正在记录鲍勃的现金取款,我们不希望在他走出银行门后,对他的账户借记在故障中消失的任何机会。事务数据库保证对事务进行的所有更新都会在报告事务完成之前记录在永久存储器(即磁盘上)。
事务数据库的另一个重要特性与原子更新的概念密切相关:当多个事务并发运行时,每个事务都应该看不到其他人做出的不完整更改。例如,如果一个事务繁忙于计算所有分行的余额,则不能只包含来自 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
包围的一组语句有时称为 事务块。
一些客户端库会自动发出 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
是重新控制因错误而被系统放入已中止状态的事务块的唯一方法,而不用完全回滚该事务并重新开始。