事务(Transactions)是所有现代企业级数据库系统的核心基石。事务的本质在于:它将多个离散的执行步骤打包为一个不可分割的、全有或全无(all-or-nothing)的原子操作。多个事务并发执行时,各步骤之间的中间不稳定状态对其他事务完全不可见;如果某个执行步骤遭遇异常导致事务无法完成,则此前执行过的所有修改都不会对数据库产生任何实质影响。
举个经典范例:考虑一个记录客户账户余额及各分支行总存款额的银行系统数据库。假设我们要记录一笔从 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');
这些 SQL 命令的具体写法在此并不重要,关键在于完成这项看似简单的业务需要接连执行数条独立的更新语句。银行管理团队必须确保这些更新要么全部成功执行,要么全部不发生。若在执行中途因系统突发故障,导致 Bob 成功收到了 100 美元而 Alice 却未被扣款,银行将产生账务亏空;反之若 Alice 被扣款而 Bob 未到账,用户权益将严重受损。我们需要绝对的确定性保障:操作执行过程中一旦出现意外,先前已经执行的步骤必须全部自动回滚。将这些更新包裹在一个事务中即可提供这种保障。事务具有原子性(atomic):从外部并发事务的视角来看,该批操作要么整体全部生效,要么如同从未发生过一样。
我们还需要另一项关键保障:一旦事务成功提交并获得数据库系统的成功响应,该笔变更即被永久落盘固化,即使服务器紧接着发生断电崩溃,数据也绝不会遗失。例如在记录 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 包裹的一组语句常被称为事务块(transaction block)。
部分第三方客户端连接驱动(如特定的 JDBC/ODBC 驱动或应用框架)会自动替你开启和提交事务,从而在无感知的情况下产生事务块效果。请注意核阅所用语言驱动的官方连接配置说明。
通过引入保存点(savepoints),可以在事务块内部实现更细粒度的控制。保存点允许你有选择地丢弃事务中从某一点开始的部分操作,同时继续保留并提交其余部分。通过 SAVEPOINT 定义保存点后,若发生局部异常,可通过 ROLLBACK TO 回滚到该保存点。在定义保存点之后所做的所有数据库修改将被完全撤销,但保存点之前已执行的操作得以保留。
在回滚到某个保存点后,该保存点依然有效存在,因此可以根据需要多次反复回滚至该点。反之,如果你确定不再需要回滚到某个特定保存点,可以显式释放它,以便系统回收内部资源。需要注意的是,无论是释放还是回滚到某个较早的保存点,在其之后定义的所有较晚保存点都将被系统自动一并释放注销。
上述所有操作均在事务块内部发生,对其他并发会话完全透明不可见。直到整个事务块成功执行提交,所有生效的修改才作为一个统一整体瞬间对其他会话可见;而中途被回滚的操作在外部看来则从未存在过。
依然以上述银行转账为例:假设我们从 Alice 账户扣除 100 美元并入账到 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';
-- 发现转账目标有误,回滚到保存点
ROLLBACK TO my_savepoint;
UPDATE accounts SET balance = balance + 100.00
WHERE name = 'Wally';
COMMIT;
该示例当然属于教学场景的高度简化,但它展现了保存点对事务内部流程控制的强大能力。此外,当事务块因执行出错而被系统置为中断故障状态(aborted state)时,除了完全放弃整个事务重头来过之外,ROLLBACK TO 是恢复事务控制权并继续执行后续有效操作的唯一途径。