受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发版本: devel

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.00美元,而Alice那边却没有被扣款。反过来,如果Alice被扣了款而Bob没有入账,她也不会满意。我们需要保证:如果操作进行到一半出了问题,到目前为止已执行的步骤都不会生效。把这些更新归为一个事务,就能提供这种保证。事务被称为原子的:从其他事务的角度看,它要么完整发生,要么根本不发生。

我们还希望保证,一旦事务完成并得到数据库系统确认,它确实已经被永久记录下来,即便紧接着发生崩溃也不会丢失。例如,如果我们正在记录Bob的一次现金提款,我们当然不希望他刚走出银行大门,对他账户的扣款就在崩溃后消失。事务型数据库保证,在把事务报告为完成之前,会先将该事务所做的全部更新记入永久存储(即磁盘)。

事务型数据库的另一个重要性质与原子更新的概念密切相关:当多个事务并发运行时,每个事务都不应该看到其他事务尚未完成的更改。例如,如果某个事务正在统计所有营业网点的余额,就不能让它只算进Alice所在网点的扣款,却没有算进Bob所在网点的入账,反过来也不行。因此,事务不仅在对数据库的持久影响上必须是全有或全无的,在其发生过程中的可见性上也必须如此。一个未结束事务到目前为止所做的更新,对其他事务不可见;等到该事务完成时,所有更新会同时变得可见。

PostgreSQL中,可以通过将事务中的 SQL 命令置于BEGINCOMMIT命令之间来建立一个事务。因此,我们的银行事务实际上会是这样:

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

如果事务执行到一半时,我们决定不想提交了(例如刚刚发现Alice的余额变成了负数),就可以发出ROLLBACK而不是COMMIT,这样到目前为止的全部更新都会被取消。

PostgreSQL实际上把每条 SQL 语句都视为在一个事务中执行。如果你没有显式发出BEGIN,那么每条独立语句外围都会隐式包上一对BEGIN和(若成功)COMMIT。由BEGINCOMMIT包围起来的一组语句,有时称为事务块

Note

某些客户端库会自动发出BEGINCOMMIT命令,因此你可能在没有主动要求的情况下,也得到了事务块的效果。请查阅你所使用接口的文档。

也可以借助保存点,以更细粒度控制事务中的语句。保存点允许你有选择地丢弃事务的一部分,同时提交其余部分。使用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;否则就只能把整个事务完全回滚并重新开始。

提交更正

如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。