3.4. 事务
事务是所有数据库系统的一个基本概念。一次事务的要点就是把多个步骤捆绑成一个单一的、 不成功则成仁的操作。其它并发的事务是看不到在这些步骤之间的中间状态的,并且如果发生了一些问题, 导致该事务无法完成,那么所有这些步骤都完全不会影响数据库。
比如,假设一个银行的数据库包含各种客户帐户的余额,以及每个分行的总余额。 假设我们要记录一次从 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 收到 100 块不是 Alice 支付的钱,也不希望 Alice 老是不花钱从 Bob 那里拿到物品。我们需要保证: 如果在操作的过程中出了差错,那么所有这些步骤都不会发生效果。 把这些更新组合成一个事务就给予我们这样的保证。事务被认为是原子的: 从其它事务的角度来看,它要么是全部发生,要么完全不发生。
我们还需要保证:一旦一个事务完成并且得到数据库系统的认可,那么它必须被真正永久地存储, 并且不会在随后的崩溃中消失。比如,如果我们记录到了一个 Bob 撤单的动作, 那么我们不希望仅仅在他走出银行大门之后的一次崩溃就会导致对他的帐户的扣减动作消失。 一个事务型数据库保证一个事务所做的所有更新在事务发出完成响应之前都记录到永久的存储中(也就是磁盘)。
事务型数据库的另外一个重要的性质和原子更新的概念关系密切:当多个事务并发地运行的时候, 每个事务都不应看到其它事务所做的未完成的变化。比如,如果一个事务正忙着计算所有分行的余额总和, 那么它不应该包括来自 Alice 的分行的扣帐和来自 Bob 分行的入帐,反之亦然。所以事务必须是黑白分明的, 不仅仅体现在它们在数据库上产生的永久影响上,而且体现在它们运转时的自身的可视性上。 一个打开的事务所做的更新在它完成之前是无法被其它事务看到的,而到提交的时候所有更新同时可见。
在PostgreSQL里,一个事务是通过把 SQL 命令用BEGIN
和COMMIT
命令包围实现的。因此我们的银行事务实际上看起来像下面这样:
BEGIN;
UPDATE accounts SET balance = balance - 100.00
WHERE name = 'Alice';
-- 等等
COMMIT;
如果在该事务的过程中,我们决定不做提交(可能是我们刚发现 Alice 的余额是负数), 那么我们可以发出ROLLBACK
而不是COMMIT
命令, 那么到目前为止我们的所有更新都会被取消。
PostgreSQL 实际上把每个 SQL 语句当做在一个事务中执行来看待。 如果你没有发出BEGIN
命令,那么每个独立的语句都被一个隐含的BEGIN
和(如果成功的话)COMMIT
包围。一组包围在BEGIN
和COMMIT
之间的语句有时候被称做事务块。
Note: 一些客户端库自动发出
BEGIN
和COMMIT
, 因此你可能不需要特意请求就可以获得事务块的效果。查看你使用的接口的文档。
我们可以通过使用保存点的方法,在一个事务里更加精细地控制其中的语句。 保存点允许你选择性地抛弃事务中的某些部分,而提交剩下的部分。在用SAVEPOINT
定义了一个保存点后,如果需要,你可以使用ROLLBACK TO
回滚到该保存点。 则该事务在定义保存点到 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';
-- 呀!加错钱了,应该用 Wally 的账号
ROLLBACK TO my_savepoint;
UPDATE accounts SET balance = balance + 100.00
WHERE name = 'Wally';
COMMIT;
这个例子当然是实在太简单了,但是通过使用保存点,我们可以对事务块有大量的控制。 并且,ROLLBACK TO
是除了事务全部回滚,重新来过之外, 唯一可以用于重新控制一个因错误而被系统置于退出状态事务的方法。