6.2 控制事务
事务的概念是关系数据库范例的一部分。事务由一个或者多个DML语句组成,后面紧跟ROLLBACK或者COMMIT命令。可以在事务内使用SAVEPOINT命令给出控制程度。在讨论语法之前,有必要回顾一下事务的概念。相关主题是读一致性;这由Oracle服务器自动实现,但某种程度上,编程人员可以使用SELECT语句来管理它。
6.2.1 数据库事务
Oracle确保事务完整性的机制是撤消段和重做日志文件的组合:此机制无疑是迄今为止开发的任何数据库中的翘楚,而且完全符合数据处理的国际标准。虽然其他数据库供应商能够通过使用自己的机制遵循相同的标准,但是改变了效率级别。简而言之,任何关系数据库都必须能够通过ACID测试,必须确保原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
1.A表示原子性
原子性原则规定一个事务的所有部分必须都完成,或都不完成。例如,如果业务分析师认为每次某个雇员的薪水发生变化时都必须同时更改该雇员的等级,那么这个“原子”事务就由两个更新操作组成。数据库必须保证两个更新操作都能完成或者都不能完成。如果只有一个更新操作被成功执行,那么雇员的薪水与等级会出现矛盾(商业术语为数据损坏)。如果事务在完成之前出现任何错误,那么数据库自身就必须保证自动回滚所做过的任何事务部分(并且必须自动进行)。虽然原子事务听起来像原子一样小,然而事实上它可以非常大。再举一个例子,从逻辑上看,某个财务软件的名目账项总账不能为八、九月份各半月的账目之和,因此月末结转就是一个原子事务,这个事务可能影响数千个表中的几百万条记录,并且需要花费几个小时才能完成(或者在出现错误时所进行的回滚)。未完成事务的回滚可以是人工进行的(此时需要执行ROLLBACK命令),不过在出现错误的情况下,回滚必须是自动的和不可停止的。
2.C表示一致性
一致性的原则规定查询的结果必须与数据库在查询开始时的状态一致。假设要进行一个简单的查询,该查询会求取某个表中某列数据的平均值。如果这个表很大,那么扫描整个表需要数分钟时间。在这个查询进行期间,如果其他用户也在更新该列,那么查询应当包括新值还是旧值?查询应当包含在查询启动之后被插入或被删除的记录吗?一致性的原则要求数据库确保查询不会看见改变后的值;无论这个查询经过多长时间或者发生与指定表相关的其他任何动作,它都会提供查询开始时指定列所有数据的平均值。
Oracle使用撤消段来保证:在查询成功的前提下,查询结果是一致的。不过,如果没有正确地配置撤消段,查询就不会成功,就会产生一个常见的Oracle错误:“ORA-1555 snapshot too old”。对于旧版本而言,这是一个极难解决的问题,不过从9i版本之后,应能避免这个问题。
3.I表示隔离性
隔离性的原则规定对于其他部分来说,未完成的(也就是未提交的)事务必须不可见。在某个事务进行期间,只有执行该事务的一个会话能看见所做的变更,而其他所有会话看见的都是没有变更的数据(而不是更改后的新值)。这个规定的逻辑含义是:首先,由于整个事务可能没有全部完成(前面介绍了原子性原则),因此不允许其他用户看到可能反转的变更。其次,在某个事务进行期间,数据是不一致的,也就是说在雇员的薪水发生变化后的短暂时间里,其薪水等级尚未发生相应的变化。事务隔离性要求数据库必须对其他用户隐藏正在进行的事务,这些用户只能看到没有更新的数据,只有在事务完成时,他们才能看到作为一个一致集合的所有变更。
Oracle使用撤消段确保事务隔离性、一致性和原子性。
4.D表示持久性
持久性的原则规定一旦某个事务完成,数据库就不可能丢失这个事务。在事务进行期间,隔离性的原则要求除了指定会话涉及的用户之外的任何用户都不能查看当前所做的变更。不过事务一旦完成,所有用户都必须能够立即看到所做的变更,同时数据库必须保证这些变更绝不会丢失(关系数据库不允许丢失数据)。Oracle通过使用日志文件来满足上述需求。日志文件有两种形式:联机重做日志文件和归档重做日志文件,其中保存了应用于数据库的每个变更。当然,数据可能因为用户错误(如不适当的DML、删除或者截断表)而丢失。但是对于Oracle和DBA而言,这些事件和其他事务一样:依据持久性原则,它们绝对不可逆。
6.2.2 执行SQL语句
整个SQL语言只有大约十几个命令。我们关心的是下列一些命令:SELECT、INSERT、UPDATE和DELETE。
1.执行SELECT语句
使用SELECT命令可以检索数据。一条SELECT语句分阶段执行。执行SELECT语句的服务器进程会首先查看包含所需数据的数据块是否已经位于内存和数据库缓冲区缓存中。如果条件成立,将立即执行语句。如果条件不成立,那么服务器必须在磁盘上定位相应的数据块,并且将这些数据块复制到数据库缓冲区缓存。
考点:
需要牢记:服务器进程将数据块从数据文件读取至数据库缓冲区缓存,而DBWn进程则将数据块从数据库缓冲区缓存写入数据文件。
只要查询所需的数据块位于数据库缓冲区缓存内,那么进一步的处理(如排序或聚合)就在指定会话的PGA中完成。执行完成后,结果集返回至用户进程。
SELECT语句的执行怎样才能与ACID测试联系在一起?就一致性而言,如果查询遇到在查询开始之后被改变的数据块,那么服务器进程会进入保护这个变更的撤消段,从而定位原有数据和回滚该变更(这个回滚操作只是针对当前的查询)。这样,在查询开始之后启动的任何变更都不可见。相似的机制确保事务隔离,但这根据是否已经提交更改,而非根据数据是否已经更改。显然,如果需要回滚的数据不再位于撤消段中,那么这种机制会失效,此时就会出现“snapshot too old”错误消息。
图6-4显示了SELECT语句的处理方式。
图6-4 SELECT的执行阶段
在图6-4中,步骤1将SELECT语句从用户进程传输给服务器进程。服务器将搜索数据库缓冲区缓存,了解必需的块是否已经在内存中,如果在,就进入到步骤4。否则,进入步骤2在数据文件中查找块,然后在步骤3中将它们复制到数据库缓冲区缓存中。步骤4将数据传输到服务器进程,做进一步处理,此后,步骤5将查询结果返回给用户进程。
2.执行UPDATE语句
对于任何DML操作来说,必须同时处理数据块和撤消块,并且还会生成重做数据。ACID测试的A、C和I要求生成撤消数据,D则要求生成重做数据。
考点:
“撤消”不是“重做”的逆过程!无论是否更改了表段、索引段或撤消段的数据块,重做都可以保护所有的数据块变更。就重做而言,撤消段只是另一个段,对这个段所做的任何变更都必须是持久的。
执行DML语句的第一个步骤与执行SELECT语句的第一个步骤相同:必须在数据库缓冲区缓存中找到所需的数据块或者将所需的数据块从数据文件复制至数据库缓冲区缓存。在这个步骤中,二者唯一的区别是执行DML语句还需要某个撤消段的一个空数据块(或者过期的数据块)。接下来,DML语句的执行就比较复杂。
首先,必须在DML操作影响的所有记录以及相关索引键上放置锁。这部分内容将在本章后面予以讨论。
接下来会生成重做数据,此时服务器进程在日志缓冲区中写入即将应用于指定数据块的变更向量。这个重做生成操作应用于表块的变更和撤消块的变更。如果要更新某个列,那么这个列的rowid和新值(将要应用于表数据块的变更)以及旧值(将要应用于撤消块的变更)都会被写入日志缓冲区。如果这个列是某个索引键的一部分,那么要应用于该索引的变更以及保护索引变更的撤消块变更也会被写入日志缓冲区。
生成重做数据后,就可以在数据库缓冲区缓存内完成下列更新操作:使用变更后的列更新表数据块,未变更的列则被写入撤消段数据块。从这一时刻到提交更新操作的时候,与发生变更的记录相关的其他会话中的所有查询都会被重定向至撤消数据。只有执行更新操作的会话才可以在表数据块中看见更新后的行。所有关联的索引变更同样也会应用上述原则。
3.执行INSERT和DELETE语句
从概念上看,INSERT和DELETE语句与UPDATE语句具有相同的管理方式。第一步是在数据库缓冲区缓存查找相关块,如果它们不在那里,就将它们复制到数据库缓冲区缓存中。
重做生成操作完全相同,应用于数据块和撤消块的所有变更向量都会首先被写入日志缓冲区。对于INSERT,将要应用于表块(还可能包括索引块)的变更向量是构成新行(还可能包括新索引键)的字节。将要应用于撤消块的向量是新行的rowid。对于DELETE,要写入到撤消块的变更向量是整个行。
INSERT和DELETE语句的重要差异在于所生成的撤消数据量不同。插入一条记录时,生成撤消的操作只涉及为撤消块写下新的rowid。这是由于回滚INSERT语句时,Oracle只需要rowid信息,因此可以构建如下语句:
delete from table_name where rowid=rowid_of_the_new_row;
执行此语句将撤消原始更改。
对于DELETE语句来说,因为整个行(可能多达数KB)都被写入撤消块,因此,可以通过构建将完整行插入回表的语句,根据需要回滚这个删除操作。
4.事务的开始和结束
会话发出DML命令时,就开始事务。事务持续执行任何数量的DML命令,直到会话发出COMMIT或者ROLLBACK语句为止。只有提交的变更才会变得永久,才会对其他会话可见。事务不能嵌套,SQL标准不允许用户启动一个事务,然后在终止第一个事务之前再启动另一个事务。使用PL/SQL(Oracle专有的第三代语言)可以这样做,但它不是行业标准的SQL。
显式事务控制语句有COMMIT、ROLLBACK和SAVEPOINT。除了用户发出的COMMIT或者ROLLBACK之外,还有一些情况会隐式终止事务:
● 发出DDL或者DCL语句
● 退出用户工具(SQL*Plus、SQL Developer或者其他工具)
● 客户会话终止
● 系统崩溃
如果用户发出DDL (CREATE、ALTER或DROP)或者DCL(GRANT或REVOKE)命令,就会提交正在处理的事务(如果有的话):会让它变得永久,并且对其他所有用户都可见。这是因为DDL和DCL命令本身都是事务。因为在SQL中不可能嵌套事务,所以如果用户已经在运行事务,就会提交用户已经运行的语句,以及组成DDL或DCL命令的语句。
如果用户通过发出DML命令来启动事务,然后在没有显式发出COMMIT或者ROLLBACK命令的情况下退出正在使用的工具,事务就会终止——但是使用COMMIT终止还是ROLLBACK终止,则完全取决于如何写工具。许多工具有不同的行为,这取决于退出工具的方式(在Microsoft Windows中,从窗口左上角的菜单中选择File | Exit选项或者单击右上角的“×”,都可以终止程序。写该工具的编程人员已经给这些功能编写了不同的逻辑)。不管在哪种情况下,都应该是受控制的退出,因此编程人员应该发出COMMIT或者ROLLBACK命令,但他们必须进行选择。
如果客户的会话因为某种原因而失败,那么数据库总是会回滚事务。这种失败可能有多种原因:用户进程终止或者在操作系统级别被终止,到数据库服务器的网络连接中断,或者运行该客户工具的计算机崩溃。在所有这些情况中,都没有依次发出COMMIT或ROLLBACK语句,现在轮到数据库来检测发生的情况。行为是终止会话,回滚活动的事务。如果失败发生在服务器端,其行为也是这样。如果数据库服务器因为某种原因崩溃,那么当它下一次启动时,就会回滚会话中所有之前正在处理的事务。
6.2.3 事务控制:COMMIT、ROLLBACK、SAVEPOINT和SELECT FOR UPDATE
Oracle的关系数据库范例的实现通过第一个DML语句隐式开始执行某个事务。事务一直延续到COMMIT或ROLLBACK语句执行为止。SAVEPOINT命令不是SQL标准的一部分,实际上是编程人员反向撤消一些语句的简便方式。不需要单独考虑它,它不终止事务。
1.COMMIT命令
许多人(甚至某些经验丰富的DBA)在提交处理这个环节上都会出现不完全或者完全错误地理解Oracle体系结构的情况。执行COMMIT命令时发生的所有物理操作是LGWR进程将日志缓冲区的内容刷新到磁盘。DBWn进程完全没有执行任何操作。对于Oracle数据库来说,这是一个非常重要的性能特性。
考点:
执行COMMIT命令时,DBWn进程不会进行任何操作。
为了使某个事务持久,所需的全部工作是将组成这个事务的变更写入磁盘(不必使实际表数据存在于磁盘的数据文件上)。如果变更以多重重做日志文件的形式存在于磁盘上,那么在出现使数据库受损的事件时,通过从数据库受损前所做的备份中还原数据文件以及应用重做日志中的变更,就可以重新实例化事务。后面的章节将详细讨论这个过程。在这里,只需要了解执行COMMIT命令只涉及将日志缓冲区的内容写入磁盘以及标记事务的完成。基于上述原因,虽然某个事务涉及数千个表中数百万个总共需要耗费若干分钟(甚至若干小时)的更新操作,但是这个事务可以在不到一秒钟的时间内被提交。因为LGWR进程的写入操作接近于实时完成,所以事实上事务的所有变更都已在磁盘上就绪。执行COMMIT命令时,LGWR进程会进行几乎实时的写操作,会话在写操作期间则会被挂起。这个延迟的时间为将日志缓冲区内的所有数据刷新到磁盘的时间(大约只需要几毫秒)。随后,会话可以继续进行。从这个时候起,如果一致性原则不做要求,那么涉及已更改表的其他所有会话都将不再被重定向至这个表曾经使用过的撤消块。
写入重做日志的变更向量是所有的变更向量:应用于数据块(表和索引)的更改以及应用于撤消段的更改。
考点:
重做日志流包含所有更改:应用于数据段和撤消段的更改(针对已提交和未提交的事务)。
通过LGWR进程写入重做日志文件的重做流包含了被提交的事务以及未被提交的事务,这很容易造成混淆。此外,在任何时刻,DBWn进程都可能将或不将数据段或撤消段的变更数据块写入用于已提交事务和未提交事务的数据文件。因此,磁盘上的数据库通常会存在受损的情况,数据文件完全可能存储未被提交的事务,也可能丢失已提交的变更。但是,在出现崩溃时,磁盘上的重做流始终具有足够的信息,通过这些信息不仅能重新实例化任何数据文件中不存在的已提交事务(使用应用于数据块的变更),而且能重新实例化数据文件中回滚未提交事务所需的撤消段(使用应用于撤消块的变更)。
考点:
任何DDL命令、GRANT或REVOKE都将提交当前事务。
2.ROLLBACK命令
在事务处理过程中,Oracle会保存事务处理之前数据的映像。在事务处理过程中,会将这个映像提供给查询数据的其他会话。如果出现错误,或者会话故意请求回滚,那么它也可以用来自动回滚事务。请求回滚的语法如下所示:
ROLLBACK [TO SAVEPOINT savepoint] ;
下一节将详细讨论保存点的可能用法。
回滚之前数据的状态是,数据已经改变,但反转这些变更所需的信息是可用的。为了满足隔离性原则,会将这些信息提供给其他所有会话。回滚会恢复数据改变之前的映像,从而抛弃所有变更;事务插入的所有行都会删除,事务删除的所有行都会重新插入表中,已经更新的行会回到原始状态。其他会话根本不知道发生了什么,它们绝对看不到这些变更。处理事务的会话现在会将数据看成事务开始之前的数据。
3.SAVEPOINT命令
使用保存点就是允许编程人员在事务中设置一个标记,这个标记可以用来控制ROLLBACK命令的效果。这个标记不是回滚整个事务并终止它,而是反转在特定点之后所做的所有变更,同时保持在该点之前所做的变更不变。事务本身继续执行:仍然没有提交,仍然可回滚,仍然对其他会话不可见。其语法如下所示:
SAVEPOINT savepoint;
该语句在事务中创建了一个命名点,在后面的ROLLBACK命令中可以使用它。表6-1说明了在事务执行的各个阶段表的行数。这个表名为TAB,它非常简单,只有一列。
表6-1 事务执行的各个阶段表的行数
表中的示例说明了两个事务:第一个使用COMMIT终止,第二个用ROLLBACK终止。可见只有在事务内才能看到保存点的使用:其他会话什么也看不到(如果没有提交)。
4.SELECT FOR UPDATE
最后一个事务控制语句是SELECT FOR UPDATE。默认情况下,Oracle提供最高级别的并发性:读者不打断写者,写者也不打断读者。或者简单地说,一个会话查询另一个会话正在更新的数据,或者一个会话更新另一个会话正在查询的数据,这都没有问题。然而,有时需要改变这种行为,防止改变正在被查询的数据。
应用程序使用SELECT命令检索一组行,将它们提供给用户精读,并给用户提示所做的变更,这种情况不是不常见。因为Oracle是一个多用户数据库,所以另一个会话也要检索这些行也不是不可能。如果这两个会话都要作出变更,那么会出现一些奇怪的效果。表6-2描述了这种情况。
表6-2 会话变更示例
这是第一个用户从SQL*Plus提示中看到的结果:
SQL> select * from regions; REGION_ID REGION_NAME ---------- ------------------------- 5 UK 1 Europe 2 Americas 3 Asia 4 Middle East and Africa SQL> update regions set region_name='GB' where region_id=5; 0 rows updated.
这使人感到有点茫然。解决这个问题的方法之一就是锁定用户感兴趣的行:
select * from regions for update;
FOR UPDATE子句会锁定所有检索的行。除了发出命令的会话之外,其他任何会话都不能改变它们,因此后面的更新操作就会成功:这些行不可能已经被改变过。这意味着一个会话有一致的数据视图(不会改变),但付出的代价是如果其他会话要更新锁定的行,它们就会挂起(当然,它们可以查询这些行)。
在发出命令的会话发出COMMIT或者ROLLBACK命令之前,会一直保持FOR UPDATE子句设置的锁定。必须这样来释放锁定,即使没有执行DML命令。
5.所谓的“自动提交”
在结束对提交处理的讨论之前,我们有必要阐明一下经常被提及的“自动提交”(有时也被称为“隐式提交”)。经常会听到这样的说法:Oracle在某些情况下可以进行“自动提交”。执行DDL语句就是其中一种情况(如前所述),退出某个用户进程(如SQL*Plus)则是另一种情况。
“自动提交”纯属子虚乌有。执行某条DDL语句时,实现这个DDL命令的源代码包含了一个完全正规的COMMIT命令。但是,退出用户进程时的情况怎样呢?如果在Windows终端上使用SQL*Plus并执行一条DML语句,然后再执行“退出”命令,就会提交事务。这是因为SQL*Plus中的“退出”命令嵌入了一条COMMIT语句。但是,单击SQL*Plus窗口的右上角会出现怎样的情况呢?此时,关闭SQL*Plus窗口,如果再次登录SQL*Plus,你会发现已回滚了事务。这是因为为Microsoft Windows编写SQL*Plus的编程人员在关闭SQL*Plus窗口的代码中嵌入了一条ROLLBACK语句。SQL*Plus在其他平台上的行为可能有所不同,唯一的确认方法是进行测试。因此,以不同方式退出某个程序时是否能够进行“自动提交”完全取决于编程人员如何编写用户进程。Oracle服务器只是按照指令进行操作。
有一个SQL*Plus命令SET AUTOCOMMIT ON。这将导致SQL*Plus修改其行为:它将COMMIT语句追加到发出的每个DML语句。因此,所有语句都将在执行后立即提交,而无法回滚。但这仅发生在用户进程端;数据库仍无自动提交的说法,长期运行的语句所做的更改在语句完成前将与其他会话隔离。当然,如果在这些环境中无序地退出SQL*Plus,如在语句运行时使用操作系统实用程序将其取消,PMON将检测到这些情况,而且处于活动状态的事务始终被回滚。
练习6-4 解释DML和撤消数据的生成
本练习将示范事务隔离和控制。使用两个SQL*Plus会话(也可以使用SQL Developer),都以用户SYSTEM的身份进行连接。按正确顺序运行两个会话在表6-3中的命令。
表6-3 执行会话的命令