事务
关系数据库必须能够对 SQL 语句进行分组,以将它们一起提交,将其应用于数据库;或者将它们全部回滚,将其全部撤消。 事务是一个逻辑、 原子的工作单元,包含一条或多条 SQL 语句。
举一个需要使用事务的例子,将资金从一个储蓄帐户转账到一个支票帐户。这笔转账由以下几个单独的操作组成:
- 从储蓄帐户中扣除资金。
- 将资金增加到支票帐户。
- 在事务日志中记录该事务。
PostgreSQL 数据库确保将所有三个操作作为一个单元来处理, 要么全部成功,要么全部失败。 例如, 如果硬件故障阻止了事务中的某个语句的执行,那么其他语句必须回滚。
事务是 PostgreSQL 数据库有别于文件系统的特征之一。 如果您执行一个原子操作,更新了几个文件,但是系统在处理过程中失败,那么文件将处于不一致状态。 相比之下,事务则会将 PostgreSQL 数据库从一个一致状态转移到另一个一致状态。 事务的基本原则是,“要么全做,要么全都不做”: 一个原子操作作为一个整体,要么都成功,要么都失败。
事务是包含一个或多个 SQL 语句的逻辑的、 原子的工作单元。事务将 SQL 语句分组,以便它们可以一起被提交,即将其应用到数据库,或者一起被回滚,即将其从数据库中撤消。PostgreSQL 数据库将为每个事务分配一个称为事务 ID 的唯一标识符。
所有 PostgreSQL 事务都符合称为 ACID 属性的数据库事务的基本属性。ACID 是以下的缩写:
-
原子性
事务中的所有任务,要么全部执行,要么都不执行。不存在部分完成的事务。例如,如果事务启动并欲更新 100 行,但在系统在完成 20 行更新后出现故障,则数据库会回滚这 20 行更新。
-
一致性
事务会将数据库从一个一致状态变为另一个一致状态。例如, 在从一个储蓄账户借记、并从一个支票账户贷记的银行事务中,故障一定不能导致数据库仅仅贷记一个账户,这会导致数据不一致。
-
隔离性
一个事务必须在被提交之后,其它事务才能看见其效果。例如,正在更新 hr.employees 表的一个用户,不会看到由另一个用户在 employees 表上未提交的更改。因此,对这些用户来说,这些事务好像是串行执行的。
-
持久性
已提交的事务所做的更改是永久性的。事务完成后,数据库通过其恢复机制,确保在事务中所做的更改不会丢失。
使用事务是数据库管理系统有别于文件系统的最重要方式之一。
为了说明事务的概念,考虑一个银行数据库。当一个客户将钱从储蓄账户转移到支票帐户时,事务必须由三个单独的操作组成:
-
减少储蓄账户余额
-
增加支票账户余额
-
在事务日志中记录这笔交易
PostgreSQL 必须考虑两种情况。如果所有三个 SQL 语句都对该账号维持了正确的余额,则事务的效果可以应用到数据库中。但是,如果某个问题,如资金不足、帐号无效、或硬件故障,阻止了事务中某些语句的完成,则数据库必须回滚整个事务,以便所有帐户的余额是正确的。
图 1 说明了银行事务。第一条语句从储蓄帐户 3209 中减去 500 美元。第二条语句向支票帐户 3208 增加 500 美元。第三条语句在日志表中插入这笔转账记录。最后一个语句提交事务。
数据库事务由一个或多个语句组成,它们一起构成对数据库的一个原子更改。其中的语句可以是数据操作语言 (DML) 语句,也可以是数据定义语言 (DDL) 语句。
事务有一个开始点和结束点。
在 PostgreSQL 中,开启一个事务需要将 SQL 命令用BEGIN
和COMMIT
命令包围起来。因此我们的银行事务看起来会是这样:
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
命令,因此我们可能会在不被告知的情况下得到事务块的效果。具体请查看所使用的接口文档。
当事务开始时,Redrock Postgres 数据库将为事务分配一个可用的回滚段,以记录新事务的撤消条目。数据库在第一个 DML/DDL 语句过程中,首先会分配回滚段和事务表槽,然后分配事务 ID。事务 ID 对于事务来说是唯一的,并由回滚段号、事务槽号、和序列号来表示。
下面的示例执行一个UPDATE
语句以开始一个事务,并查询有关事务的详细信息:
BEGIN;
UPDATE hr.employees SET salary = salary;
SELECT xid, pg_xact_status(xid) AS status
FROM pg_current_xact_id_if_assigned() AS xid;
xid | status
----------+-------------
(5,16,1) | in progress
事务在出现以下操作时结束:
-
用户在没有
SAVEPOINT
子句的语句中发出COMMIT
或ROLLBACK
。提交意味着用户会显式或隐式地请求事务中所做的更改被持久化。只有在提交事务之后,事务所做的更改才是永久性的,并对其他用户可见。图 1 所示的事务以一个提交来结束。
-
用户从大多数 PostgreSQL 数据库实用程序和工具正常退出,导致当前事务被隐式提交。当用户断开连接时的提交行为依赖于应用程序,并且是可配置的。
注意:在程序终止之前,应用程序应始终显式地提交或回滚其事务。
-
客户端进程异常终止,导致数据库使用存储在事务表和回滚段中的元数据来隐式回滚其事务。
一个事务结束后, 下一个可执行的 SQL 语句将自动启动后续事务。下面的示例执行一个更新,来启动一个事务,并以一个ROLLBACK
语句来结束事务,然后又执行一个更新启动一个新事务(请注意,事务 ID 变了):
BEGIN;
UPDATE hr.employees SET salary = salary;
SELECT xid, pg_xact_status(xid) AS status
FROM pg_current_xact_id_if_assigned() AS xid;
xid | status
----------+-------------
(6,16,1) | in progress
ROLLBACK;
SELECT pg_current_xact_id_if_assigned() AS xid;
xid
-----
BEGIN;
UPDATE hr.employees SET last_name = last_name;
SELECT xid, pg_xact_status(xid) AS status
FROM pg_current_xact_id_if_assigned() AS xid;
xid | status
----------+-------------
(7,16,1) | in progress
Redrock Postgres 支持语句级原子性,这意味着 SQL 语句是一个原子工作单元,要么完全成功,要么完全失败。
成功的语句不同于已提交事务。如果一个 SQL 语句能被数据库正确分析,并作为一个原子单位来运行且未产生错误,且所有行都被正确更改,则这个 SQL 语句是成功执行的。
如果 SQL 语句在执行过程中导致错误,则它是不成功的,该语句中的所有影响都将被回滚。此操作是一个语句级回滚。此操作具有以下特征:
-
未成功的 SQL 语句只会导致它本身执行的工作丢失。
未成功的语句不会导致丢失当前事务中该语句之前的任何工作。例如,若图1中第二个
UPDATE
语句的执行导致错误并被回滚,但由第一个UPDATE
语句执行的工作是不会被回滚的。第一个UPDATE
语句可以由用户显式提交或回滚。 -
回滚的效果是,好像该语句从来未运行过。
原子语句的副作用被视为原子语句的一部分,例如,由语句执行所引发的触发器调用。作为原子语句的一部分所产生的所有工作,要么都成功,要么都失败。
由错误导致语句级回滚的一个示例,是尝试插入重复的主键。死锁中的某个为争用相同数据的 SQL 语句,也会导致语句级回滚。然而,在解析中发现的错误,比如语法错误,实际上尚未运行,因此不会导致语句级回滚。
逻辑时间戳(logicaltime
)是一个由 Redrock Postgres 数据库使用的逻辑、 内部的时间戳。logicaltime
按数据库中发生的事件排序,以满足在事务 ACID 属性的需要。Redrock Postgres 数据库使用logicaltime
来标记某个位置,在其之前的所有更改都被认为已写到磁盘上,以避免在恢复过程中应用不必要的重做。数据库还使用logicaltime
来标记一个其数据不存在重做信息的点,以便恢复过程可以在该点停止。
logicaltime
是按单调递增的顺序发生的。Redrock Postgres 数据库可以像使用时钟一样使用 logicaltime
,因为一个logicaltime
观察值指示一个逻辑时间点,而其重复观察值相比之前会相同或更大。若一个事件的logicaltime
比另一个事件的logicaltime
低,则它在数据库中发生在一个更早的时间。几个事件可以共享相同的logicaltime
, 这意味着他们在数据库中同时发生。
每个事务都有一个logicaltime
。例如,如果事务更新了一行,则数据库记录此更新发生时的 logicaltime
。此事务中其他修改具有相同的logicaltime
。当事务提交时,数据库将为此提交记录一个logicaltime
。
Redrock Postgres 数据库在共享内存区域中递增logicaltime
。事务中修改数据时,数据库会将一个新的logicaltime
写入到分配给事务的回滚段。然后日志写入器进程立即将事务的提交记录写入在线重做日志。提交记录具有事务的唯一 logicaltime
。Redrock Postgres 数据库还使用 logicaltime
作为其实例恢复和介质恢复机制的一部分。
事务控制即管理 SQL 语句所做的更改,和将 SQL 语句分组为事务。一般情况下,应用程序设计人员都关注事务控制,以便工作能按逻辑单元来完成,且数据能保持一致。
事务控制涉及使用下面的语句:
BEGIN
语句开始一个事务块,也就是说所有BEGIN
语句之后的所有语句将在一个事务中执行,直到给出一个显式的COMMIT
或者ROLLBACK
。COMMIT
语句结束当前事务,并使在事务中执行的所有更改都具有持久性。提交还会清除在事务中的所有保存点,并释放事务锁。ROLLBACK
语句将取消当前事务中所做的工作;它导致所有自上次提交或回滚以来的数据更改被丢弃。ROLLBACK TO SAVEPOINT
语句将撤消自上次保存点以来所做的更改,但不会结束整个事务。SAVEPOINT
语句标识在事务中您可以稍后回滚到的点。
会话表 1 说明了事务控制的基本概念。
表 1 事务控制
时间 | 会话 | 解释 |
---|---|---|
t0 | BEGIN; | 此语句开始一个事务块。 |
t1 | UPDATE employees SET salary = 7000 WHERE last_name = ‘Banda’; | 此语句将 Banda 的薪金更新为 7000。 |
t2 | SAVEPOINT after_banda_sal; | 此语句创建一个名为after_banda_sal 的保存点,使该事务中的更改可以回滚到该点。 |
t3 | UPDATE employees SET salary = 12000 WHERE last_name = ‘Greene’; | 此语句将Greene的薪金更新为 12000。 |
t4 | SAVEPOINT after_greene_sal; | 此语句创建一个名为after_greene_sal 的保存点,使该事务中的更改可以回滚到该点。 |
t5 | ROLLBACK TO SAVEPOINT after_banda_sal; | 此语句将事务回滚至 t2,撤消在 t3 时对 Greene 薪金的更新。此时事务并未结束。 |
t6 | UPDATE employees SET salary = 11000 WHERE last_name = ‘Greene’; | 此语句在事务中将 Greene 的薪金更新为11000 。 |
t7 | ROLLBACK; | 此语句回滚事务中的所有更改,以结束事务。 |
t8 | BEGIN; | 此语句开始一个新的事务块。 |
t9 | UPDATE employees SET salary = 7050 WHERE last_name = ‘Banda’; | 此语句将 Banda 的薪金更新为 7050。 |
t10 | UPDATE employees SET salary = 10950 WHERE last_name = ‘Greene’; | 此语句将 Greene 的薪金更新为 10950。 |
t11 | COMMIT; | 此语句提交事务中的所有更改,以结束事务。提交保证所做的更改被保存在预写式日志文件中。 |
活动事务即是已开始但尚未提交或回滚的事务。在表 1 中,事务中第一个修改数据的语句是更新 Banda 的薪金。从该更新成功执行、到 ROLLBACK
语句结束该事务,事务一直处于活动状态。
在事务提交或回滚之前,事务对数据所做的更改是暂时的。在事务结束之前,数据的状态如下所示:
-
Redrock Postgres 数据库已在共享缓冲区中生成撤消数据信息。
撤消数据包含由事务中的 SQL 语句所更改的数据旧值。
-
Redrock Postgres 数据库已在预写式日志缓冲区中生成了重做信息。
重做日志记录包含数据块和撤消块的更改。
-
已经对共享缓冲区中的数据页面进行了更改。
已提交事务所做的数据更改,存储在共享缓冲区中,但不一定会立即由数据库写入器写入数据文件。磁盘写入可能会在提交之前或之后发生。
-
数据更改所影响的行已被锁定。
其他用户不能更改受影响的行中的数据,也不能查看未提交的更改。
保存点是事务上下文中的一个由用户声明的中间标记。在内部,此标记将被解析为一个撤消记录位置。保存点将长事务划分为多个更小的部分。
如果你在一个长事务中使用保存点,则您就可以在之后将事务中执行的工作回滚到当前时间之前,却又在事务中声明的某个保存点之后。因此,如果您出了错,您不需要重新提交所有每个语句。表1创建保存点after_banda_sal
,以便对 Greene 薪金的更新可以回滚到此保存点。
在未提交事务中回滚到某个保存点,意味着该指定的保存点之后所做的任何更改都将被撤消,但它并不意味着事务本身的回滚。当一个事务回滚到一个保存点时,比如像表1中运行的ROLLBACK TO SAVEPOINT after_banda_sal
,将执行以下操作:
-
Redrock Postgres 数据库将只回滚在该保存点之后运行的语句。
在表 1 中,
ROLLBACK TO SAVEPOINT
将导致对 Greene 的更新被回滚,但对 Banda 的更新不会被回滚。 -
Redrock Postgres 数据库保留在
ROLLBACK TO SAVEPOINT
语句中指定的保存点,但随后的所有保存点都将丢失。在表 1 中,
ROLLBACK TO SAVEPOINT
子句将导致after_greene_sal
保存点丢失。 -
Redrock Postgres 数据库释放在指定保存点之后获取的所有表锁和行锁,但保留在该保存点之前获取的所有数据锁。
事务仍处于活动状态,并可以继续。
未提交事务的回滚,将撤消已由事务中的 SQL 语句执行过的任何数据更改。事务回滚后,事务中所做工作的影响就不再存在。
对未引用任何保存点的整个事务的回滚,Redrock Postgres 数据库执行下列操作:
-
通过使用相应的回滚段,撤消事务中所有 SQL 语句所做的所有更改
每个活动事务的事务表条目包含一个指向该事务的所有撤消数据 (与应用顺序相反)的指针。数据库从回滚段中读取数据,反转其操作,然后将撤消条目标记为已应用。因此,如果一个事务插入行,则其回滚删除行。如果一个事务更新行,则其回滚反转这个更新。如果一个事务删除一个行,则其回滚重新插入该行。在表1中,
ROLLBACK
反转对 Greene 和 Banda 的薪金更新。 -
释放由事务持有的所有数据锁
-
清除在事务中的所有保存点
在表 1 中,
ROLLBACK
删除保存点after_banda_sal
。after_greene_sal
保存点被ROLLBACK TO SAVEPOINT
语句删除。 -
结束事务
在表 1 中,
ROLLBACK
使数据库处于与刚刚执行了COMMIT
后相同的状态。
回滚的持续时间与被修改的数据量成正比。
提交结束当前事务,并使事务中执行的所有更改持久化。在表1中,第二个事务以BEGIN
语句开始,并以显式COMMIT
语句结束。在两个 UPDATE
语句中所做的更改现在被持久化了。
当一个事务提交时,将发生以下操作:
-
为提交生成逻辑时间戳 (
logicaltime
)。在关联的回滚段的内部事务表中记录已提交的事务。分配相应的唯一事务
logicaltime
,并将其记录在事务表中。 -
将预写式日志缓冲区中的剩余重做日志条目写入到在线 WAL 日志中,并将事务
logicaltime
写入到在线 WAL 日志中。这个原子事件是提交事务的本质所在。 -
Redrock Postgres 数据库释放在行和表上持有的锁。
还在排队等待由未提交事务持有的锁的用户,现在可以继续他们的工作了。
-
Redrock Postgres 数据库删除保存点。
在表 1 中,事务提交时不存在保存点,所以也没有保存点要清除。
-
Redrock Postgres 数据库执行一个提交清理。
如果包含已提交的事务的被修改数据块仍处于共享缓冲区中,也没有其他会话正在对他们进行修改,则数据库从该块中删除与锁相关的事务信息。理想情况下,
COMMIT
会清理该数据块,以便后续的SELECT
操作不必再执行此任务。注意:由于数据块清理会生成重做,所以查询可能生成重做,并因此导致在下一个检查点期间该数据块会被写入。
-
Redrock Postgres 数据库将事务标记为完成。
事务提交后,用户可以查看所做的更改。
通常,提交是一个快速的操作,而与事务大小无关。提交的速度不会随着在事务中修改的数据的大小的增大而增加。提交的最长部分是由 刷写预写式日志触发的物理磁盘 I/O。然而,刷写预写式日志所用的时间量却减少了,因为walwriter
进程一直在在后台以增量方式写出重做日志缓冲区中的内容。
默认行为是,会话必须将重做数据同步地写入在线 WAL 日志,而事务必须在将被缓冲的重做数据写到磁盘上之后,提交操作才能返回到用户。但是,为取得更低的事务提交延迟,应用程序开发人员可以指定重做被异步写入,以便事务不需要等待重做被写入磁盘,就可以立即从COMMIT
调用返回。