10. 事务
10.1. 事务简介
事务是包含一个或多个SQL语句的逻辑、原子工作单元。事务将SQL语句分组,使它们要么全部提交,这意味着它们被应用到数据库中,要么全部回滚,这意味着它们从数据库中被撤销。Oracle数据库为每个事务分配一个唯一的标识符,称为事务ID。
所有Oracle事务都符合数据库事务的基本属性,即ACID属性。ACID是以下各项的缩写:
-
原子性
事务的所有任务都执行,或者都不执行。没有部分事务。例如,如果一个事务开始更新100行,但在更新20行后系统失败,那么数据库将回滚这20行的更改。 -
一致性
事务将数据库从一个一致的状态转换到另一个一致的状态。例如,在一笔银行交易中,从储蓄账户中借记并在支票账户中贷记,任何故障都不应导致数据库只贷记一个账户,这将导致数据不一致。 -
隔离性
事务的效果在事务提交之前对其他事务不可见。例如,一个用户正在更新hr.employees表,他不会看到另一个用户同时对员工所做的未提交的更改。因此,对用户来说,就好像事务是串行执行的。 -
持久性
已提交事务所做的更改是永久性的。事务完成后,数据库通过其恢复机制确保事务的更改不会丢失。
使用事务是数据库管理系统与文件系统不同的重要方式之一。
10.1.1. 示例事务:账户借记和贷记
为了说明事务的概念,考虑一个银行数据库。当客户从储蓄账户转账到支票账户时,事务必须包括三个独立的操作:
- 减少储蓄账户的金额
- 增加支票账户的金额
- 在交易日志中记录交易
Oracle数据库必须允许两种情况。如果所有三个SQL语句都能保持账户的正确余额,那么事务的效果可以应用到数据库中。然而,如果出现问题,如资金不足、账户号码无效或硬件故障,阻止事务中的一个或两个语句完成,那么数据库必须回滚整个事务,以确保所有账户的余额正确。
图10-1说明了一笔银行交易。第一条语句从储蓄账户3209中减去500美元。第二条语句向支票账户3208中增加500美元。第三条语句将转账记录插入到日志表中。最后的语句提交事务。
10.1.2. 事务的结构
数据库事务由一个或多个语句组成。具体来说,事务包括以下之一:
- 一个或多个数据操纵语言(DML)语句,它们共同构成对数据库的原子性更改
- 一个数据定义语言(DDL)语句
事务有一个开始和一个结束。
10.1.2.1. 事务开始
当遇到第一个可执行的SQL语句时,事务开始。可执行的SQL语句是生成对数据库实例调用的SQL语句,包括DML和DDL语句以及SET TRANSACTION语句。当事务开始时,Oracle数据库将事务分配给一个可用的回滚数据段,以记录新事务的回滚条目。直到第一个DML语句执行期间分配了回滚段和事务表槽,才会分配事务ID。事务ID对于事务是唯一的,它代表回滚段号、槽位和序列号。以下示例执行一个UPDATE语句来开始一个事务,并查询V$TRANSACTION以获取有关事务的详细信息:
SQL> UPDATE hr.employees SET salary=salary;
107 rows updated.
SQL> SELECT XID AS "txn id", XIDUSN AS "undo seg", XIDSLOT AS "slot", 2 XIDSQN AS "seq", STATUS AS "txn status" 3 FROM V$TRANSACTION;
txn id undo seg slot seq txn status
---------------- ---------- ---------- ---------- ---------------
0600060037000000 6 6 55 ACTIVE
10.1.2.2. 事务结束
当以下任何操作发生时,事务结束:
- 用户发出带有或不带SAVEPOINT子句的COMMIT或ROLLBACK语句。
在提交操作中,用户明确或隐式地请求使事务中的更改永久化。事务所做的更改只有在事务提交后才对其他用户永久和可见。图10-1中显示的事务以提交结束。 - 用户运行如CREATE、DROP、RENAME或ALTER等DDL命令。
数据库在每个DDL语句之前和之后发出隐式的COMMIT语句。如果当前事务包含DML语句,则Oracle数据库首先提交事务,然后作为一个新的单一语句事务运行并提交DDL语句。 - 用户正常退出大多数Oracle数据库实用程序和工具,导致当前事务被隐式提交。用户断开连接时的提交行为取决于应用程序并可配置。
注意:应用程序在程序终止前应始终显式提交或撤销事务。
- 客户端进程异常终止,导致事务使用存储在事务表和回滚段中的元数据被隐式回滚。
一个事务结束后,下一个可执行的SQL语句自动开始下一个事务。以下示例执行一个UPDATE语句来开始一个事务,用ROLLBACK语句结束事务,然后执行一个UPDATE来开始一个新的事务(注意事务ID是不同的):
SQL> UPDATE hr.employees SET salary=salary;
107 rows updated.
SQL> SELECT XID, STATUS FROM V$TRANSACTION;
XID STATUS
---------------- ---------------
0800090033000000 ACTIVE
SQL> ROLLBACK;
Rollback complete.
SQL> SELECT XID FROM V$TRANSACTION;
no rows selected
SQL> UPDATE hr.employees SET last_name=last_name; 107 rows updated.
SQL> SELECT XID, STATUS FROM V$TRANSACTION;
XID STATUS
---------------- ---------------
0900050033000000 ACTIVE
10.1.3. 语句级原子性
Oracle数据库支持语句级原子性,这意味着SQL语句是工作的一个原子单元,要么完全成功,要么完全失败。
成功的语句与提交的事务不同。如果数据库将单个SQL语句作为原子单元解析并运行,没有错误,那么这个语句就成功执行了,比如在多行更新中所有行都已更改。
如果SQL语句在执行过程中出现错误,那么它就不成功,因此该语句的所有效果都会被回滚。这个操作是语句级回滚。这个操作有以下特点:
- 一个不成功的SQL语句只会导致它自己将要执行的工作丢失。
不成功的语句不会导致当前事务中它之前的任何工作丢失。例如,如果图10-1中第二个UPDATE语句执行时出现错误并被回滚,那么第一个UPDATE语句所做的工作不会被回滚。用户可以显式提交或回滚第一个UPDATE语句。 - 回滚的效果就好像该语句从未运行过一样。
原子语句的任何副作用,例如,在执行语句时调用的触发器,都被视为原子语句的一部分。原子语句生成的所有工作要么全部成功,要么全部不成功。
导致语句级回滚的一个错误示例是尝试插入重复的主键。参与死锁的单个SQL语句,即对相同数据的竞争,也可能导致语句级回滚。然而,在SQL语句解析期间发现的错误,如语法错误,尚未运行,因此不会导致语句级回滚。
10.1.4. 系统变更号(SCNs)
系统变更号(SCN)是Oracle数据库使用的逻辑内部时间戳。SCNs对数据库中发生的事件进行排序,这对于满足事务的ACID属性是必要的。Oracle数据库使用SCNs来标记所有已知更改已在磁盘上的所有更改之前的那个SCN,以便恢复避免应用不必要的重做。数据库还使用SCNs来标记没有一组数据的重做存在的点,以便恢复可以停止。SCNs以单调递增的序列出现。Oracle数据库可以像使用时钟一样使用SCN,因为观察到的SCN指示了一个逻辑时间点,重复观察返回相等或更大的值。如果一个事件的SCN比另一个事件的SCN低,那么它在数据库方面发生在更早的时间。几个事件可能共享同一个SCN,这意味着它们在数据库方面发生在同一时间。
每个事务都有一个SCN。例如,如果一个事务更新了一行,数据库就记录了这个更新发生的SCN。此事务中的其他修改具有相同的SCN。当事务提交时,数据库为此提交记录一个SCN。
Oracle数据库在系统全局区域(SGA)中增加SCN。当事务修改数据时,数据库将新的SCN写入分配给该事务的回滚数据段。然后日志写入进程立即将事务的提交记录写入在线重做日志。提交记录具有事务的唯一SCN。Oracle数据库还使用SCN作为其实例恢复和介质恢复机制的一部分。
10.2. 事务控制概述
事务控制是对DML语句所做的更改进行管理,并将DML语句分组到事务中。通常,应用程序设计者关心事务控制,以便以逻辑单元完成工作并保持数据一致性。
事务控制涉及使用以下语句,如第7-8页“事务控制语句”中所述:
- COMMIT语句:结束当前事务,并将事务中执行的所有更改永久化。COMMIT还会删除事务中的所有保存点并释放事务锁。
- ROLLBACK语句:撤销当前事务中完成的工作;它导致自上次COMMIT或ROLLBACK以来的所有数据更改被丢弃。ROLLBACK TO SAVEPOINT语句撤消自最后一个保存点以来的更改,但不会结束整个事务。
- SAVEPOINT语句:标识事务中的一个点,以后可以回滚到该点。
表10-1中的会话说明了事务控制的基本概念。
10.2.1. 事务名称
事务名称是可选的、用户指定的标签,用作提醒事务正在执行的工作。您可以使用SET TRANSACTION ... NAME语句为事务命名,如果使用,它必须是事务的第一个语句。在第10-6页的表10-1中,第一个事务被命名为sal_update,第二个被命名为sal_update2。
事务名称提供以下优势:
- 更容易监控长时间运行的事务,并解决不确定的分布式事务。
- 您可以在应用程序中查看事务名称和事务ID。例如,数据库管理员可以在监控系统活动时,在Oracle Enterprise Manager(企业经理)中查看事务名称。
- 数据库将事务名称写入事务审计重做记录,因此您可以使用LogMiner在重做日志中搜索特定事务。
- 您可以使用事务名称在数据字典视图(如V$TRANSACTION)中找到特定事务。
10.2.2. 活跃事务
活跃事务已启动但尚未提交或回滚。在第10-6页的表10-1中,sal_update事务中第一个修改数据的语句是对Banda薪水的更新。从该更新成功执行到ROLLBACK语句结束事务期间,sal_update事务处于活跃状态。
事务所做的数据更改在事务提交或回滚之前是临时的。在事务结束之前,数据的状态如下:
- Oracle数据库在系统全局区域(SGA)中生成了回滚数据的信息。回滚数据包含事务的SQL语句所更改的旧数据值。详见第9-7页的“读取已提交隔离级别中的读取一致性”。
- Oracle数据库在SGA的在线重做日志缓冲区中生成了重做。重做日志记录包含对数据块的更改和对回滚块的更改。详见第14-14页的“重做日志缓冲区”。
- 数据库的SGA缓冲区已进行了更改。已提交事务的数据更改存储在SGA的数据库缓冲区中,数据库写入器(DBW)不一定会立即将其写入数据文件。磁盘写入可以在提交之前或之后发生。详见第14-9页的“数据库缓冲区缓存”。
- 受数据更改影响的行被锁定。其他用户无法更改受影响行中的数据,也无法看到未提交的更改。详见第9-12页的“锁定行为总结”。
10.2.3. 保存点
保存点是用户在事务上下文中声明的中间标记。在内部,这个标记解析为一个系统变更号(SCN)。保存点将长事务划分为较小的部分。
如果您在长事务中使用保存点,那么您以后可以选择回滚在事务的当前点之前但在事务中声明的保存点之后执行的工作。因此,如果您犯了一个错误,您不需要重新提交每个语句。第10-6页的表10-1在更新Greene薪水后创建了一个名为after_banda_sal的保存点,这样就可以将更新回滚到这个保存点。
10.2.3.1. 回滚至保存点
在未提交的事务中回滚到保存点意味着撤销指定保存点之后所做的任何更改,但这并不意味着事务本身的回滚。当事务回滚到保存点时,如下所示:
-
Oracle数据库仅回滚保存点之后运行的语句。在第10-6页的表10-1中,ROLLBACK TO SAVEPOINT导致Greene的UPDATE被回滚,但Banda的UPDATE没有被回滚。
-
Oracle数据库保留ROLLBACK TO SAVEPOINT语句中指定的保存点,但所有后续的保存点都会丢失。在第10-6页的表10-1中,ROLLBACK TO SAVEPOINT导致after_greene_sal保存点丢失。
-
Oracle数据库释放指定保存点之后获得的所有表和行锁,但保留在保存点之前获得的所有数据锁。事务保持活跃状态,并可以继续进行。
事务仍然处于活跃状态,并且可以继续执行。
10.2.3.2. 排队事务
根据场景的不同,等待之前锁定资源的事务在回滚到保存点后可能仍然被阻塞。当一个事务被另一个事务阻塞时,它会在阻塞事务上排队,这样,只有当阻塞事务提交或回滚时,被阻塞的事务才能继续进行。
在表10-2所示的场景中,会话1回滚到执行DML语句之前创建的保存点。然而,会话2仍然被阻塞,因为它正在等待会话1的事务完成。
10.2.4. 事务回滚
回滚一个未提交的事务会撤销事务内SQL语句对数据所做的任何更改。事务回滚后,事务中完成的工作效果将不再存在。
在回滚整个事务时,不引用任何保存点,Oracle数据库执行以下操作:
- 撤销事务中所有SQL语句所做的所有更改,通过使用相应的回滚段。每个活跃事务的事务表条目包含指向事务的所有回滚数据(以应用的相反顺序)的指针。数据库从回滚段读取数据,反转操作,然后标记回滚条目为已应用。因此,如果一个事务插入了一行,那么回滚就会删除它。如果一个事务更新了一行,那么回滚就会撤销更新。如果一个事务删除了一行,那么回滚会重新插入它。在第10-6页的表10-1中,ROLLBACK撤销了对Greene和Banda薪水的更新。
- 释放事务持有的所有数据锁。在第10-6页的表10-1中,ROLLBACK删除了保存点after_banda_sal。after_greene_sal保存点被ROLLBACK TO SAVEPOINT语句移除。
- 结束事务。在第10-6页的表10-1中,ROLLBACK使数据库处于执行初始COMMIT后的状态。
- 回滚的持续时间取决于修改的数据量。
10.2.5. 事务提交
提交(COMMIT)结束当前事务,并将事务中执行的所有更改永久化。在第10-6页的表10-1中,第二个事务以sal_update2开始,并以显式的COMMIT语句结束。两个UPDATE语句所产生的更改现在变得永久。当事务提交时,会发生以下操作:
-
为COMMIT生成一个系统变更号(SCN)。与相关回滚表空间的内部事务表记录事务已提交。事务的唯一SCN被分配并记录在事务表中。详见第9-8页的“可序列化隔离级别”。
-
日志写入(LGWR)进程将重做日志缓冲区中剩余的重做日志条目写入在线重做日志,并将事务SCN写入在线重做日志。这个原子事件构成了事务的提交。
-
Oracle数据库释放行和表上的锁。那些在未提交事务持有的锁上排队等待的用户被允许继续他们的工作。
-
Oracle数据库删除保存点。在第10-6页的表10-1中,sal_update事务中没有保存点,因此没有保存点被删除。
-
Oracle数据库执行提交清理。如果SGA中仍然包含来自已提交事务的数据的修改块,并且没有其他会话正在修改它们,那么数据库会从块中移除与锁相关的事务信息。理想情况下,COMMIT会清理块,以便后续的SELECT不必执行此任务。
注意:由于块清理会产生重做,查询可能会生成重做,从而导致在下一个检查点期间块被写入。
- Oracle数据库标记事务为完成。事务提交后,用户可以查看更改。
通常,提交操作是快速的,无论事务的大小如何。提交的速度不会随着事务中修改的数据量的增加而增加。提交中耗时最长的部分是LGWR执行的物理磁盘I/O。然而,LGWR所花费的时间减少了,因为它在后台逐步写入重做日志缓冲区的内容。默认情况下,LGWR以同步方式将重做写入在线重做日志,事务在返回提交给用户之前会等待缓冲的重做写入磁盘。然而,为了降低事务提交的延迟,应用程序开发人员可以指定以异步方式写入重做,这样事务就不必等待重做写入磁盘,并可以立即从COMMIT调用返回。
10.3. 自治事务
自治事务是可以从另一个称为主事务的事务中调用的独立事务。您可以挂起调用事务,执行SQL操作并在自治事务中提交或撤销它们,然后恢复调用事务。自治事务适用于必须独立执行的操作,无论调用事务是提交还是回滚。例如,在股票购买事务中,无论股票购买是否成功,您都希望提交客户数据。此外,即使整体事务回滚,您也希望将错误消息记录到调试表中。自治事务具有以下特点:
- 自治事务看不到主事务所做的未提交更改,也不与主事务共享锁或资源。
- 自治事务中的更改在自治事务提交后对其他事务可见。因此,用户可以在不必等待主事务提交的情况下访问更新的信息。
- 自治事务可以启动其他自治事务。除了资源限制之外,没有限制可以调用多少级别的自治事务。
在PL/SQL中,自治事务在标记有pragma AUTONOMOUS_TRANSACTION的自治作用域内执行。在此上下文中,例程包括顶级匿名PL/SQL块和PL/SQL子程序以及触发器。Pragma是一种指示编译器执行编译选项的指令。Pragma AUTONOMOUS_TRANSACTION指示数据库,当执行此过程时,将其作为一个新的自治事务执行,该事务独立于其父事务。图10-2显示了控制流如何从主例程(MT)流向自治例程,然后再返回。主例程是proc1,自治例程是proc2。自治例程在控制权返回给主例程之前可以提交多个事务(AT1和AT2)。
当您进入自治例程的可执行部分时,主例程将暂停。当您退出自治例程时,主例程将恢复。
在图10-2中,proc1内的COMMIT不仅使其自己的工作永久化,还使其会话中执行的任何未完成的工作永久化。然而,在proc2中的COMMIT仅使proc2事务中执行的工作永久化。因此,事务AT1和AT2中的COMMIT语句对MT事务没有影响。
10.4. 分布式事务
分布式数据库是一组在分布式系统中的数据库,这些数据库可以对应用程序呈现为单一的数据源。分布式事务是一个事务,其中包含一个或多个语句,这些语句使用称为数据库链接的模式对象在两个或多个不同的分布式数据库节点上更新数据。数据库链接描述了一个数据库实例如何登录到另一个数据库实例。
与本地数据库上的事务不同,分布式事务会在多个数据库上更改数据。因此,分布式事务处理更为复杂,因为数据库必须协调事务中更改的提交或回滚,以确保其作为一个原子单元执行。整个事务必须要么提交,要么回滚。Oracle数据库必须在网络上协调事务控制,并保持数据一致性,即使发生网络或系统故障。
10.4.1. 两阶段提交
两阶段提交机制确保所有参与分布式事务的数据库要么全部提交,要么全部撤销事务中的语句。该机制还保护了由完整性约束、远程过程调用和触发器执行的隐式DML。
在多个数据库之间的两阶段提交中,一个数据库协调分布式事务。发起节点被称为全局协调器。协调器询问其他数据库是否准备好提交。如果任何数据库回答“不”,则整个事务将回滚。如果所有数据库都投票“是”,则协调器广播一条消息,使每个数据库上的提交永久化。
两阶段提交机制对于发出分布式事务的用户是透明的。实际上,用户甚至不需要知道事务是分布式的。表示事务结束的COMMIT语句会自动触发两阶段提交机制。在数据库应用程序的主体中包含分布式事务不需要编码或复杂的语句语法。
10.4.2. 不确定事务
当两阶段提交由于任何类型的系统或网络故障而中断时,就会发生不确定的分布式事务。例如,两个数据库向协调数据库报告它们已准备好提交,但协调数据库实例在收到消息后立即失败。这两个准备提交的数据库现在被悬挂起来,等待通知结果。
恢复器(RECO)后台进程自动解决不确定的分布式事务的结果。在故障修复并重新建立通信后,每个本地Oracle数据库的RECO进程会自动在所有涉及的节点上一致地提交或回滚任何不确定的分布式事务。
如果发生长期故障,Oracle数据库允许每个本地管理员手动提交或撤销由于故障而不确定的任何分布式事务。这个选项允许本地数据库管理员释放由于长期故障而无限期持有的任何锁定资源。
如果数据库必须恢复到过去的时间,那么数据库恢复设施使其他站点的数据库管理员能够将他们的数据库恢复到早期的时间点。此操作确保全局数据库保持一致。