Oracle 9i & 10g编程艺术-深入数据库体系结构——第7章:并发与多版本

来源:互联网 发布:阿里巴巴农村淘宝 编辑:程序博客网 时间:2024/06/05 03:55

第7章                      并发与多版本

上一章曾经说过,开发多用户的数据库驱动应用时,最大的难题之一是:一方面要力争最大的并发访问,与此同时还要确保每个用户能以一致的方式读取和修改数据。这一章我们将进一步详细地讨论Oracle如何获得多版本读一致性(multi-version read consistency),并说明这对于开发人员来说意味着什么。我还会介绍一个新概念,即写一致性(write consistency),并用这个概念来说明Oracle不仅能在提供读一致性的读环境中工作,还能在混合读写环境中工作。

7.1   什么是并发控制?

并发控制concurrency control)是数据库提供的函数集合,允许多个人同时访问和修改数据。前一章曾经说过,锁(lock)是Oracle管理共享数据库资源并发访问并防止并发数据库事务之间“相互干涉”的核心机制之一。总结一下,Oracle使用了多种锁,包括:

q         TX锁:修改数据的事务在执行期间会获得这种锁。

q         TM锁和DDL锁:在你修改一个对象的内容(对于TM锁)或对象本身(对应DDL锁)时,这些锁可以确保对象的结构不被修改。

q         闩(latch):这是Oracle的内部锁,用来协调对其共享数据结构的访问。

不论是哪一种锁,请求锁时都存在相关的最小开销。TX锁在性能和基数方面可扩缩性极好。TM锁和DDL锁要尽可能地采用限制最小的模式。闩和队列锁(enqueue)都是轻量级的,而且都很快(在这二者中,队列锁相对“重”一些,不过,它的功能也更丰富)。如果应用设计不当,不必要地过长时间保持锁,而导致数据库中出现阻塞,就会带来问题。如果能很好地设计代码,利用Oracle的锁定机制就能建立可扩缩的高度并发的应用。

但是Oracle对并发的支持不只是高效的锁定。它还实现了一种多版本(multi-versioning)体系结构(见第1章的介绍),这种体系结构提供了一种受控但高度并发的数据访问。多版本是指,Oracle能同时物化多个版本的数据,这也是Oracle提供数据读一致视图的机制(读一致视图即read-consistent view,是指相对于某个时间点有一致的结果)。多版本有一个很好的副作用,即数据的读取器(reader)绝对不会被数据的写入器(writer)所阻塞。换句话说,写不会阻塞读。这是Oracle与其他数据库之间的一个根本区别。在Oracle中,如果一个查询只是读取信息,那么永远也不会被阻塞。它不会与其他会话发生死锁,而且不可能得到数据库中根本不存在的答案。

注意   在分布式2PC(两段提交)的处理期间,在很短的一段时间内Oracle不允许读信息。因为这种处理相当少见,而且属于例外情况(只有查询在准备阶段和提交阶段之间开始,而且试图在提交之前读取数据,此时才会存在这个问题),所以我不打算详细介绍这种情况。

默认情况下,Oracle的读一致性多版本模型应用于语句级(statement level),也就是说,应用于每一个查询;另外还可以应用于事务级(transaction level)。这说明,至少提交到数据库的每一条SQL语句都会看到数据库的一个读一致视图,如果你希望数据库的这种读一致视图是事务级的(一组SQL语句),这也是可以的。

数据库中事务的基本作用是将数据库从一种一致状态转变为另一种一种状态。ISO SQL标准指定了多种事务隔离级别(transaction isolation level),这些隔离级别定义了一个事务对其他事务做出的修改有多“敏感”。越是敏感,数据库在应用执行的各个事务之间必须提供的隔离程度就越高。在下一节中我们就看到,Oracle如何利用多版本体系结构和绝对最小锁定(absolutely minimal locking)来支持SQL标准定义的各种隔离级别。

7.2   事务隔离级别

ANSI/ISO SQL标准定义了4种事务隔离级别,对于相同的事务,采用不同的隔离级别分别有不同的结果。也就是说,即使输入相同,而且采用同样的方式来完成同样的工作,也可能得到完全不同的答案,这取决于事务的隔离级别。这些隔离级别是根据3个“现象”定义的,以下就是给定隔离级别可能允许或不允许的3种现象:

q         脏读(dirty read):这个词不仅不好听,实际上也确实是贬义的。你能读取未提交的数据,也就是脏数据。只要打开别人正在读写的一个OS文件(不论文件中有什么数据),就可以达到脏读的效果。如果允许脏读,将影响数据完整性,另外外键约束会遭到破坏,而且会忽略惟一性约束。

q         不可重复读(nonrepeatable read):这意味着,如果你在T1时间读取某一行,在T2时间重新读取这一行时,这一行可能已经有所修改。也许它已经消失,有可能被更新了,等等。

q         幻像读(phantom read):这说明,如果你在T1时间执行一个查询,而在T2时间再执行这个查询,此时可能已经向数据库中增加了另外的行,这会影响你的结果。与不可重复读的区别在于:在幻像读中,已经读取的数据不会改变,只是与以前相比,会有更多的数据满足你的查询条件。

注意      ANSI/ISO SQL标准不只是定义了单个的语句级特征,还定义了事务级特征。在后面几页的介绍中,我们将分析事务级隔离,而不只是语句级隔离。

SQL隔离级别是根据以下原则定义的,即是否允许上述各个现象。我发现有一点很有意思,SQL标准并没有强制采用某种特定的锁定机制或硬性规定的特定行为,而只是通过这些现象来描述隔离级别,这就允许多种不同的锁定/并发机制存在(见表7-1

7-1     ANSI隔离级别

隔离级别                                  脏读               不可重复                    幻像读

READ UNCOMMITTED              允许                  允许                        允许

READ COMMITTED                                          允许                        允许

REPEATABLE READ                                                                          允许

SERIALIZABLE                                                                                  

Oracle明确地支持READ COMMITTED(读已提交)和SERIALIZABLE(可串行化)隔离级别,因为标准中定义了这两种隔离级别。不过,这还不是全部。SQL标准试图建立多种隔离级别,从而允许在各个级别上完成的查询有不同程度的一致性。REPEATABLE READ(可重复读)也是SQL标准定义的一个隔离级别,可以保证由查询得到读一致的(read-consistent)结果。在SQL标准的定义中,READ COMMITTED不能提供一致的结果,而READ UNCOMMITTED(读未提交)级别用来得到非阻塞读(non-blocking read)。

不过,在Oracle中,READ COMMITTED则有得到读一致查询所需的所有属性。在其他数据库中,READ COMMITTED查询可能(而且将会)返回数据库中根本不存在的答案(即实际上任何时间点上都没有这样的结果)。另外,Oracle还秉承了READ UNCOMMITTED的“精神”。(有些数据库)提供脏读的目的是为了支持非阻塞读,也就是说,查询不会被同一个数据的更新所阻塞,也不会因为查询而阻塞同一数据的更新。不过,Oracle不需要脏读来达到这个目的,而且也不支持脏读。但在其他数据库中必须实现脏读来提供非阻塞读。

除了4个已定义的SQL隔离级别外,Oracle还提供了另外一个级别,称为READ ONLY(只读)。READ ONLY事务相对于无法在SQL中完成任何修改的REPEATABLE READSERIALIZABLE事务。如果事务使用READ ONLY隔离级别,只能看到事务开始那一刻提交的修改,但是插入、更新和删除不允许采用这种模式(其他会话可以更新数据,但是READ ONLY事务不行)。如果使用这种模式,可以得到REPEATABLE READSERIALIZABLE级别的隔离性。

下面再来讨论多版本及读一致性如何用于实现隔离机制,并说明不支持多版本的数据库怎样得到同样的结果。如果你曾经用过其他数据库,自认为很清楚隔离级别是如何工作的,就会发现这一部分的介绍对你很有帮助。还有一点很有意思,你会看到,尽管ANSI/ISO SQL标准原本力图消除数据库之间的差别,而实际上却允许各个数据库有不同的具体做法。这个标准尽管非常详细,但是可以采用完全不同的方式来实现。

7.2.1             READ UNCOMMITTED

READ UNCOMMITTED隔离级别允许脏读。Oracle没有利用脏读,甚至不允许脏读。READ UNCOMMITTED隔离级别的根本目标是提供一个基于标准的定义以支持非阻塞读。我们已经看到了,Oracle会默认地提供非阻塞读。在数据库中很难阻塞一个SELECT查询(如前所述,只是在分布式事务中对此有一个特殊的例外情况)。每个查询都以一种读一致的方式执行,而不论是SELECTINSERTUPDATEMERGE,还是DELETE。这里把UPDATE语句称为查询可能很可笑,不过,它确实是一个查询。UPDATE语句有两个部分:一个是WHERE子句定义的读部分,另一个是SET子句定义的写部分。UPDATE语句会对数据库进行读写,就像所有DML语句一样。对此只有一个例外:使用VALUES子句的单行INSET是一个特例,因为这种语句没有读部分,而只有写部分。

在第1章中,我们通过一个简单的单表查询说明了Oracle如何得到读一致性,那个例子中获得了在游标打开后所删除的行。下面我们再分析一个实际的例子,来看看使用多版本的Oracle中会发生什么,并介绍在其他多种数据库中又会怎么样。

先来看一个简单的表和查询(还是与以前一样):

create table accounts

( account_number number primary key,

account_balance number not null

);

select sum(account_balance) from accounts;

查询开始前,数据如表7-2所示。

7-2     修改前的ACCOUNTS

                     帐号                                 账户金额

1                               123                                              $500.00

2                               456                                               $240.25

. . .                             . . .                                                   . . .

342,023                    987                                              $100.00

下面,select语句开始执行,读取第1行、第2行等。在查询中的某一点上,一个事务将$400.00从账户123转到账户987。这个事务完成了两个更新,但是并没有提交。现在的数据表如表7-3所示。

所以,其中两行已经锁定。如果有人试图更新这两行,该用户就会被阻塞。到目前为止,我们所看到的在所有数据库上基本上都是一致的。只是如果有人要查询锁定的数据,此时不同的数据库就会表现出不同的行为。

7-3     修改期间的ACCOUNTS

                    帐号                                 账户金额                                是否?

1                              123                             ($500.00) changed to $100.00                               X

2                              456                                              $240.25                                                

. . .                            . . .                                                  . . .                                                    

342,023                    987                             ($100.00) changed to $500.00                               X

如果我们执行的查询要访问某一块,其中包含表“最后”已锁定的行(第342,023行),则会注意到,这一行中的数据在开始执行之后有所改变。为了提供一个一致(正确)的答案,Oracle在这个时刻会创建该块的一个副本,其中包含查询开始时行的“本来面目”。也就是说,它会读取值$100.00,这就是查询开始时该行的值。这样一来,Oracle就有效地绕过了已修改的数据,它没有读修改后的值,而是从undo段(也称为回滚(rollback)段,详细内容见第9章)重新建立原数据。因此可以返回一致而且正确的答案,而无需等待事务提交。

再来看允许脏读的数据库,这些数据库只会返回读取那一刻在账户987中看到的值,在这里就是$500.00。这个查询会把转账的$400重复统计两次。因此,它不仅会返回错误的答案,而且会返回表中根本不存在的一个总计(任何时间点都没有这样一个总计)。在多用户数据库中,脏读可能是一个危险的特性,就我个人来看,我实在看不出脏读有什么用处。例如,如果不是转账,而是事务要向账户987中存入$400.00会怎么样呢?脏读会计入$400.00,得到“正确”的答案,是这样吗?假设未提交的事务回滚了,那么我们就计入了数据库中并没有的$400.00

这里的关键是,脏读不是一个特性:而是一个缺点。Oracle中根本不需要脏读。Oracle完全可以得到脏读的所有好处(即无阻塞),而不会带来任何不正确的结果。

7.2.2             READ COMMITTED

READ COMMITTED隔离级别是指,事务只能读取数据库中已经提交的数据。这里没有脏读,不过可能有不可重复读(也就是说,在同一个事务中重复读取同一行可能返回不同的答案)和幻像读(与事务早期相比,查询不光能看到已经提交的行,还可以看到新插入的行)。在数据库应用中,READ COMMITTED可能是最常用的隔离级别了,这也是Oracle数据库的默认模式,很少看到使用其他的隔离级别。

不过,得到READ COMMITTED隔离并不像听起来那么简单。看看表7-1,你可能认为这看上去很直接。显然,根据先前的规则,在使用READ COMMITTED隔离级别的数据库中执行的查询肯定就会有相同的表现,真的是这样吗?这是不对的。如果在一条语句中查询了多行,除了Oracle外,在几乎所有其他的数据库中,READ COMMITTED隔离都可能“退化”得像脏读一样,这取决于具体的实现。

Oracle中,由于使用多版本和读一致查询,无论是使用READ COMMITTED还是使用READ UNCOMMITTED,从ACCOUNTS查询得到的答案总是一样的。Oracle会按查询开始时数据的样子对已修改的数据进行重建,恢复其“本来面目”,因此会返回数据库在查询开始时的答案。

下面来看看其他数据库,如果采用READ COMMITTED模式,前面的例子又会怎样。你可能看到答案很让人吃惊。我们从表所述的那个时间点开始:

q         现在正处在表的中间。已经读取并合计了前N行。

q         另一个事务将$400.00从账户123转到账户987

q         事务还没有提交,所以包含账户123987信息的行被锁定。

我们知道Oracle中到达账户987那一行时会发生什么,它会绕过已修改的数据,发现它原本是$100.00,然后完成工作。表7-4显示了其他数据库(非Oracle)采用默认的READ COMMITTED模式运行时可能得到的答案。

7-4     Oracle数据库使用READ COMMITTED隔离级别时的时间表

时间                                 查询                                                   转账事务

T1      读取第1行。到目前为止Sum=$500.00                    

T2      读取第2行。到目前为止Sum=$740.25                    

T3                                                                                 更新第1行,并对第1行加一个排他锁,

                                                                                    防止出现其他更新和读取。第1行现在的

                                                                                    值为$100.00

T4      读取第N行。Sum=…                                            

T5                                                                                 更新第342,023行,对这一行加一个排他

                                                                                    锁。现在第342,023行的值是$500.00

T6    试图读取第342,023行,发现这一行被锁定。会话会阻塞,

       并等待这一行重新可用。对这个查询的所有处理都停止

T7                                                                                 提交事务

T8      读取第342,023行,发现值为$500.00,提供一个

         最终答案,可惜这里把$400.00重复计入了两次

首先要注意到,在这个数据库中,到达账户987时,我们的查询会被阻塞。这个会话必须等待这一行,真正持有排它锁的事务提交。这是因为这个原因,所以很多人养成一种坏习惯,在执行每条语句后都立即提交,而不是处理一个合理的事务,其中包括将数据库从一种一致状态转变为另一种一致状态所需的所有语句。在大多数其他数据库中,更新都会干涉读取。在这种情况下,还有一个不好的消息,我们不仅会让用户等待,而且他们苦苦等待的最后结果居然还是不正确的。我们会到数据库中从来没有过的答案,这就像脏读一样,但是与脏读不同的是,这一次还需要用户等待这个错误的答案。在下一节中,我们将了解这些数据库要得到读一致的正确结果需要做些什么。

从这里可以得到一个重要的教训,不同的数据库尽管采用相同的、显然安全的隔离级别,而且在完全相同的环境中执行,仍有可能返回完全不同的答案。要知道的重要的一点是,在Oracle中,非阻塞读并没有以答案不正确作为代价。有时,鱼和熊掌可以兼得。

7.2.3             REPEATABLE READ

REPEATABLE READ的目标是提供这样一个隔离级别,它不仅能给出一致的正确答案,还能避免丢失更新。我们会分别给出例子,来看看在Oracle中为达到这些目标需要做些什么,而在其他系统中又会发生什么。

1.      得到一致的答案

如果隔离级别是REPEATABLE READ,从给定查询得到的结果相对于某个时间点来说应该是一致的。大多数数据库(不包括Oracle)都通过使用低级的共享读锁来实现可重复读。共享读锁会防止其他会话修改我们已经读取的数据。当然,这会降低并发性。Oracle则采用了更具并发性的多版本模型来提供读一致的答案。

Oracle中,通过使用多版本,得到的答案相对于查询开始执行那个时间点是一致的。在其他数据库中,通过使用共享读锁,可以得到相对于查询完成那个时间点一致的答案,也就是说,查询结果相对于我们得到的答案的那一刻是一致的(稍后会对这个问题做更多的说明)。

在一个采用共享读锁来提供可重复读的系统中,可以观察到,查询处理表中的行时,这些行都会锁定。所以,仍使用前面的例子,也就是我们的查询要读取ACCOUNTS表,那么每一行上都会有共享读锁,如表7-5所示。

7-5     Oracle数据库使用READ REPEATABLE隔离级别时的时间表1

时间      查询                                                                              转账事务

T1         读取第1行。到目前为止Sum=$500.00

            1上有一个共享读锁

T2         读取第2行。到目前为止Sum=$740.25

            2上有一个共享读锁

T3                                                                                 试图更新第1行,但是被阻塞。这个

                                                                                    事务被挂起,直至可以得到一个排他锁

T4         读取第N行。Sum=…                                         

T5         读取第342,023行,看到$100.00,提供最后的答案

T6         提交事务                                                           

T7                                                                                 更新第1行,并对这一块加一个排他锁。

                                                                                    现在第1行有$100.00

T8                                                                                 更新第342,023行,对这一块加一个排它

                                                                                    锁。第342,023行现在的值为$500.00

                                                                                    提交事务

从表7-5可以看出,现在我们得到了正确的答案,但是这是有代价的:需要物理地阻塞一个事务,并且顺序执行两个事务。这是使用共享读锁来得到一致答案的副作用之一:数据的读取器会阻塞数据的写入器。不仅如此,在这些系统这,数据的写入器还会阻塞数据读取器。想像一下,如果实际生活中自动柜员机(ATM)这样工作会是什么情况。

由此可以看到,共享读锁会妨碍并发性,而且还导致有欺骗性的错误。在表7-6中,我们先从原来的表开始,不过这一次的目标是把$50.00从账户987转账到账户123

7-6     Oracle数据库使用READ REPEATABLE隔离级别时的时间表2

时间      查询                                                                              转账事务

T1         读取第1行。到目前为止Sum=$500.00

            1上有一个共享读锁

T2         读取第2行。到目前为止Sum=$740.25

            2上有一个共享读锁

T3                                                                                 更新第342,023行,对块342,023加一个

                                                                                    排他锁,防止出现其他更新和共享读锁。

                                                                                    现在这一行的值为$50.00

T4         读取第N行。Sum=…                                         

T5                                                                                 试图更新第1行,但是被阻塞。这个事务

                                                                                    被挂起,直至可以得到一个排他锁

T6         试图读取第342,023行,但是做不到,

            因为该行已经有一个排他锁

我们陷入了经典的死锁条件。我们的查询拥有更新需要的资源,而更新也持有着查询所需的资源。查询与更新事务陷入死锁。要把其中一个作为牺牲品,将其中止。这样说来,我们可能会花大量的时间和资源,而最终只是会失败并回滚。这是共享读锁的另一个副作用:数据的读取器和写入器可能而且经常相互死锁。

可以看到,Oracle中可以得到语句级的读一致性,而不会带来读阻塞写的现象,也不会导致死锁。Oracle从不使用共享读锁,从来不会。Oracle选择了多版本机制,尽管更难实现,但绝对更具并发性。

2.      丢失更新:另一个可移植性问题

在采用共享读锁的数据库中,REPEATABLE READ的一个常见用途是防止丢失更新。

注意      丢失更新问题的丢失更新检测及解决方案在第6章已经讨论过。

在一个采用共享读锁(而不是多版本)的数据库中,如果启用了REPEATABLE READ,则不会发生丢失更新错误。这些数据库中之所以不会发生丢失更新,原因是:这样选择数据就会在上面加一个锁,数据一旦由一个事务读取,就不能被任何其他事务修改。如此说来,如果你的应用认为REPEATABLE READ就意味着“丢失更新不可能发生”,等你把应用移植到一个没有使用共享读锁作为底层并发控制机制的数据库时,就会痛苦地发现与你预想的并不一样。

尽管听上去使用共享读锁好像不错,但你必须记住,如果读取数据时在所有数据上都加共享读锁,这肯定会严重地限制并发读和修改。所以,尽管在这些数据库中这个隔离级别可以防止丢失更新,但是与此同时,也使得完成并发操作的能力化为乌有!对于这些数据库,你无法鱼和熊掌兼得。

7.2.4             SEAIALIZABLE

一般认为这是最受限的隔离级别,但是它也提供了最高程度的隔离性。SERIALIZABLE事务在一个环境中操作时,就好像没有别的用户在修改数据库中的数据一样。我们读取的所有行在重新读取时都肯定完全一样,所执行的查询在整个事务期间也总能返回相同的结果。例如,如果执行以下查询:

Select * from T;

Begin dbms_lock.sleep( 60*60*24 ); end;

Select * from T;

T返回的答案总是相同的,就算是我们睡眠了24小时也一样(或者会得到一个ORA-1555:snapshot too old错误,这将在第8章讨论)。这个隔离级别可以确保这两个查询总会返回相同的结果。其他事务的副作用(修改)对查询是不可见的,而不论这个查询运行了多长时间。

Oracle中是这样实现SERIALIZABLE事务的:原本通常在语句级得到的读一致性现在可以扩展到事务级。

注意      前面提到过,Oracle中还有一种称为READ ONLY的隔离级别。它有着SERIALIZABLE隔离级别的所有性质,另外还会限制修改。需要指出,SYS用户(或作为SYSDBA连接的用户)不能有READ ONLYSERIALIZABLE事务。在这方面,SYS很特殊。

结果并非相对于语句开始的那个时间点一致,而是在事务开始的那一刻就固定了。换句话说,Oracle使用回滚段按事务开始时数据的原样来重建数据,而不是按语句开始时的样子重建。

这里有一点很深奥——在你问问题之前,数据库就已经知道了你要问的问题的答案。

这种隔离性是有代价的,可能会得到以下错误:

ERROR at line 1:

ORA-08177: can't serialize access for this transaction

只要你试图更新某一行,而这一行自事务开始后已经修改,你就会得到这个消息。

 

注意      Oracle试图完全在行级得到这种隔离性,但是即使你想修改的行尚未被别人修改后,也可能得到一个ORA-01877错误。发生ORA-01877错误的原因可能是:包含这一行的块上有其他行正在被修改。

Oracle采用了一种乐观的方法来实现串行化,它认为你的事务想要更新的数据不会被其他事务所更新,而且把宝押在这上面。一般确实是这样的,所以说通常这个宝是押对了,特别是在事务执行得很快的OLTP型系统中。尽管在其他系统中这个隔离级别通常会降低并发性,但是在Oracle中,倘若你的事务在执行期间没有别人更新你的数据,则能提供同等程度的并发性,就好像没有SERIALIZABLE事务一样。另一方面,这也是有缺点的,如果宝押错了,你就会得到ORA_08177错误。不过,可以再想想看,冒这个险还是值得的。如果你确实要更新信息,就应该使用第1章所述的SELECT … FOR UPDATE,这会实现串行访问。所以,如果使用SERIALIZABLE隔离级别,只要保证以下几点就能很有成效:

q         一般没有其他人修改相同的数据

q         需要事务级读一致性

q         事务都很短(这有助于保证第一点)

Oracle发现这种方法的可扩缩性很好,足以运行其所有TPC-C(这是一个行业标准OLTP基准:有关详细内容请见www.tpc.org)。在许多其他的实现中,你会发现这种隔离性都是利用共享读锁达到的,相应地会带来死锁和阻塞。而在Oracle中,没有任何阻塞,但是如果其他会话修改了我们也想修改的数据,则会得到ORA-08177错误。不过,与其他系统中得到死锁和阻塞相比,我们得到的错误要少得多。

但是,凡事总有个但是,首先必须了解存在这样一些不同的隔离级别,而且要清楚它们带来的影响。要记住,如果隔离级别设置为SERIALIZABEL,事务开始之后,你不会看到数据库中做出任何修改,直到提交事务为止。如果应用试图保证其数据完整性约束,如第1章介绍的资源调度程序,就必须在这方面特别小心。如果你还记得,第1章中的问题是:我们无法保证一个多用户系统中的完整性约束,因为我们看不到其他未提交会话做出的修改。通过使用SERIALIZABLE。这些未提交的修改还是看不到,但是同样也看不到事务开始后执行的已提交的修改!

还有最后一点要注意,SERIALIZABLE并不意味着用户执行的所有事务都表现得好像是以一种串行化方式一个接一个地执行。SERIALIZABLE不代表事务有某种串行顺序并且总能得到相同的结果。尽管按照SQL标准来说,这种模式不允许前面所述的几种现象,但不能保证事务总按串行方式顺序执行。最后这个概念经常被误解,不过只需一个小小的演示例子就能澄清。下表表示了在一段时间内完成工作的两个会话。数据库表AB开始时为空,并创建如下:

ops$tkyte@ORA10G> create table a ( x int );

Table created.

ops$tkyte@ORA10G> create table b ( x int );

Table created.

现在可以得到表7-7所示的一系列事件。

7-7      SERIALIZABLE事务例子

时间                    会话1执行                                                      会话2执行

T1        Alter session set isolation_level=

            serializable;

T2                                                                                Alter session set isolation_level=

                                                                                    serializable;

T3        Insert into a select count(*)

            from b;

T4                                                                                Insert into b select count(*) from a;

T5        Commit;

T6                                                                                Commit;

现在,一起都完成后,表AB中都有一个值为0的行。如果事务有某种“串行”顺序,就不可能得到两个都包含0值的表。如果会话1在会话2之前执行,表B就会有一个值为1的行。如果会话2在会话1之前执行,那么表A则有一个值为1的行。不过,按照这里的执行方式,两个表中的行都有值0,不论是哪个事务,执行时就好像是此时数据库中只有它一个事务一样。不管会话1查询多少次表B,计数(count)都是对T1时间数据库中已提交记录的计数。类似地,不论会话2查询多少次表A,都会得到与T2时间相同的计数。

7.2.5             READ ONLY

READ ONLY事务与SERIALIZABLE事务很相似,惟一的区别是READ ONLY事务不允许修改,因此不会遭遇ORA-08177错误。READ ONLY事务的目的是支持报告需求,即相对于某个时间点,报告的内容应该是一致的。在其他系统中,为此要使用REPEATABLE READ,这就要承受共享读锁的相关影响。在Oracle中,则可以使用READ ONLY事务。采用这种模式,如果一个报告使用50SELECT语句来收集数据,所生成的结果相对于某个时间点就是一致的,即事务开始的那个时间点。你可以做到这一点,而无需在任何地方锁定数据。

为达到这个目标,就像对单语句一样,也使用了同样的多版本机制。会根据需要从回滚段重新创建数据,并提供报告开始时数据的原样。不过,READ ONLY事务也不是没有问题。在SERIALIZABLE事务中你可能会遇到ORA-08177错误,而在READ ONLY事务中可能会看到ORA-1555snapshot too old错误。如果系统上有人正在修改你读取的数据,就会发生这种情况。对这个信息所做的修改(undo信息)将记录在回滚段中。但是回滚段以一种循环方式使用,这与重做日志非常相似。报告运行的时间越长,重建数据所需的undo信息就越有可能已经不在那里了。回滚段会回绕,你需要的那部分回滚段可能已经被另外某个事务占用了。此时,就会得到ORA-1555错误,只能从头再来。

对于这个棘手的问题,惟一的解决方案就是为系统适当地确定回滚段的大小。我多次看到,人们为了节省几MB的磁盘空间而把回滚段设置得尽可能小(这些人的想法是:“为什么要在我不需要的东西上‘浪费’空间呢?)。问题在于,回滚段是完成数据库工作的一个关键组件,除非有合适的大小,否则就会遭遇这个错误。在使用Oracle 67816年间,我可以自豪地说,除了测试或开发系统之外,我从来没有在生产系统中遇到过ORA-1555错误。如果真的遇到了这个错误,这就说明你没有正确地设置回滚段的大小,需要适当地加以修正。我们将在第9章再来讨论这个问题。

7.3   多版本读一致性的含义

到此为止,我们已经看到了多版本机制如何提供非阻塞读,我强调了多版本是一个好东西,它不仅能提供一致(正确)的答案,还有高度的并发性。能还有什么不妥吗?这么说吧,除非你了解到存在多版本机制,也知道多版本的含义,否则事务完成就有可能完成得不正确。应该记得,在第1章的调度资源例子中,我们必须采用某种手动锁定技术(通过SELECT FOR UPDATESCHEDULES表中的资源调度修改进行串行化)。但是它会在其他方面带来影响吗?答案是当然会。下面几节将具体谈谈这些问题。

7.3.1             一种会失败的常用数据仓库技术

我看到,许多人都喜欢用这样一种常用的数据仓库技术:

(1)     他们使用一个触发器维护源表中的一个LAST_UPDATED列,这与上一章的6.2.3节中讨论的方法很相似。

(2)     最初要填充数据仓库表时,他们要记住当前的时间,为此会选择源系统上的SYSDATE。例如,假设现在刚好是上午9:00

(3)     然后他们从事务系统中拉(pull)出所有行,这是一个完整的SELECT * FROM TABLE查询,可以得到最初填充的数据仓库。

(4)     要刷新这个数据仓库,他们要再次记住现在的时间。例如,假设已经过去了1个小时,现在源系统上的时间就是10:00.他们会记住这一点。然后拉出自上午9:00(也就是第一次拉出数据之前的那个时刻)以来修改过的所有记录,并把这些修改合并到数据仓库中。

注意   这种技术可能会在两次连续的刷新中将相同的记录“拉出”两次。由于时钟的粒度所致,这是不可避免的。MERGE操作不会受此影响(即更新数据仓库中现有的记录,或插入一个新记录)。

他们相信,现在数据仓库中有了自第一次执行拉出操作以来所修改的所有记录。他们确实可能有所有记录,但是也有可能不是这样。对于其他采用锁定系统的数据库来说,这种技术确实能很好地工作,在这些数据库中读会被写阻塞,反之写也会被读阻塞。但是在默认支持非阻塞读的系统中,这个逻辑是有问题的。

要看这个例子有什么问题,只需假设上午9:00至少有一个打开的未提交事务。例如,假设在上午8:59:30时,这个事务已经更新了表中我们想复制的一行。在上午9:00,开始拉数据时,会读取这个表中的数据,但是我们看不到对这一行做的修改;只能看到它的最后一个已提交的版本。如果在查询中到达这一行时它已经锁定,我们就会绕过这个锁。如果在到达它之前事务已经提交,我们还是会绕过它读取查询开始时的数据,因为读一致性只允许我们读取语句开始时数据库中已经提交的数据。在上午9:00第一次拉数据期间我们读不到这一行的新版本,在上午10:00刷新期间也读不到这个修改过的行。为什么呢?上午10:00的刷新只会拉出自那天早上上午9:00以后修改的记录,但是这个记录是在上午8:59:30时修改的,我们永远也拉不到这个已修改的记录。

在许多其他的数据库中,其中读会被写阻塞,可以完成已提交但不一致的读,那么这个刷新过程就能很好地工作。如果上午9:00(第一次拉数据时)我们到达这一行,它已经上锁,我们就会阻塞,等待这一行可用,然后读取已提交的版本。如果这一行未锁定,那么只需读取就行,因为它们都是已提交的。

那么,这是否意味着前面的逻辑就根本不能用呢?也不是,这只是说明我们需要用稍微不同的方式来得到“现在”的时间。应该查询V$TRANSACTION,找出最早的当前时间是什么,以及这个视图中START_TIME列记录的时间。我们需要拉出自最老事务开始时间(如果没有活动事务,则取当前的SYSDATE值)以来经过修改的所有记录:

select nvl( min(to_date(start_time,'mm/dd/rr hh24:mi:ss')),sysdate)

from v$transaction;

在这个例子中,这就是上午8:59:30,即修改这一行的事务开始的那个时间。我们在上午10:00刷新数据时,会拉出自那个时间以来发生的所有修改,把这些修改合并到数据仓库中,这就能得到需要的所有东西。

7.3.2             解释热表上超出期望的I/O

在另外一种情况下很有必要了解读一致性和多版本,这就是生产环境中在一个大负载条件下,一个查询使用的I/O比你在测试或开发系统时观察到的I/O要多得多,而你无法解释这一现象。你查看查询执行的I/O时,注意到它比你在开发系统中看到的I/O次数要多得多,多得简直不可想像。然后,你再在测试环境中恢复这个实例,却发现I/O又降下来了。但是到了生产环境中,它又变得非常高(但是好像还有些变化:有时高,有时低,有时则处于中间)。可以看到,造成这种现象的原因是:在你测试系统中,由于它是独立的,所以不必撤销事务修改。不过,在生产系统中,读一个给定的块时,可能必须撤销(回滚)多个事务所做的修改,而且每个回滚都可能涉及I/O来获取undo信息并应用于系统。

可能只是要查询一个表,但是这个表上发生了多个并发修改,因此你看到Oracle正在读undo段,从而将块恢复到查询开始时的样子。通过一个会话就能很容易地看到由此带来的结果,从而了解到底发生了什么。我们先从一个非常小的表开始:

ops$tkyte@ORA10GR1> create table t ( x int );

Table created.

 

ops$tkyte@ORA10GR1> insert into t values ( 1 );

1 row created.

 

ops$tkyte@ORA10GR1> exec dbms_stats.gather_table_stats( user, 'T' );

PL/SQL procedure successfully completed.

 

ops$tkyte@ORA10GR1> select * from t;

X

----------

1

下面,将会话设置为使用SERIALIZABLE隔离级别,这样无论在会话中运行多少次查询,都将得到事务开始时刻的查询结果:

ops$tkyte@ORA10GR1> alter session set isolation_level=serializable;

Session altered.

下面查询这个小表,并观察执行的I/O次数:

ops$tkyte@ORA10GR1> set autotrace on statistics

ops$tkyte@ORA10GR1> select * from t;

X

----------

1

 

Statistics

----------------------------------------------------------

0 recursive calls

0 db block gets

3 consistent gets

...

由此可见,完成这个查询用了3I/O(一致获取,consistent get)。在另一个会话中,我们将反复修改这个表:

ops$tkyte@ORA10GR1> begin

2 for i in 1 .. 10000

3 loop

4 update t set x = x+1;

5 commit;

6 end loop;

7 end;

8 /

PL/SQL procedure successfully completed

再返回到前面的SERIALIZABLE会话,重新运行同样的查询:

ops$tkyte@ORA10GR1> select * from t;

X

----------

1

Statistics

----------------------------------------------------------

0 recursive calls

0 db block gets

10004 consistent gets

...

这一次执行了10,004I/O,简直有天壤之别。那么,所有这些I/O是从哪里来的呢?这是因为Oracle回滚了对该数据库块的修改。在运行第二个查询时,Oracle知道查询获取和处理的所有块都必须针对事务开始的那个时刻。到达缓冲区缓存时,我们发现,缓存中的块“太新了”,另一个会话已经把这个块修改了10,000次。查询无法看到这些修改,所以它开始查找undo信息,并撤销上一次所做的修改。它发现这个回滚块还是太新了,然后再对它做一次回滚。这个工作会复发进行,直至最后发现事务开始时的那个版本(即事务开始时数据库中的已提交块)。这才是我们可以使用的块,而且我们用的就是这个块。

注意   需要指出,有一点很有意思,如果你想再次运行SELECT * FROM T,可能会看到I/O再次下降到3;不再是10,004。为什么呢?Oracle能把同一个块的多个版本保存在缓冲区缓存中。你撤销对这个块的修改时,也就把相应的版本留在缓存中了,这样以后执行查询时就可以直接访问。

那么,是不是只在使用SERIALIZABLE隔离级别时才会遇到这个问题呢?不,绝对不是。可以考虑一个运行5分钟的查询。在查询运行的这5分钟期间,它从缓冲区缓存获取块。每次从缓冲区缓存获取一个块时,都会完成这样一个检查:“这个块是不是太新了?如果是,就将其回滚。”另外,要记住,查询运行的时间越长,它需要的块在此期间被修改的可能性就越大。

现在,数据库希望进行这个检查(也就是说,查看块是不是“太新”,并相应地回滚修改)。正是由于这个原因,缓冲区缓存实际上可能在内存中包含同一个块的多个版本。通过这种方式,很有可能你需要的版本就在缓存中,已经准备好,正等着你使用,而无需使用undo信息进行物化。请看以下查询:

select file#, block#, count(*)

from v$bh

group by file#, block#

having count(*) > 3

order by 3

/

可以用这个查询查看这些块。一般而言,你会发现在任何时间点上缓存中一个块的版本大约不超过6个,但是这些版本可以由需要它们的任何查询使用。

通常就是这些小的“热表”会因为读一致性遭遇I/O膨胀问题。另外,如果查询需要针对易失表长时间运行,也经常受到这个问题的影响。运行的时间越长,“它们也就会运行得越久”,因为过一段时间,它们可能必须完成更多的工作才能从缓冲区缓存中获取一个块。

7.4   写一致性

到此为止,我们语句了解了读一致性:Oracle可以使用undo信息来提供非阻塞的查询和一致(正确)的读。我们了解到,查询时,Oracle会从缓冲区缓存中读出块,它能保证这个块版本足够“旧”,能够被该查询看到。

但是,这又带来了以下的问题:写/修改会怎么样呢?如果运行以下UPDATE语句,会发生什么:

Update t set x = 2 where y = 5;

在该语句运行时,有人将这条语句已经读取的一行从Y=5更新为Y=6,并提交,如果是这样会发生什么情况?也就是说,在UPDATE开始时,某一行有值Y=5。在UPDATE使用一致读来读取表时,它看到了UPDATE开始时这一行是Y=5。但是,现在Y的当前值是6,不再是5了,在更新X的值之前,Oracle会查看Y是否还是5。现在会发生什么呢?这会对更新有什么影响?

显然,我们不能修改块的老版本,修改一行时,必须修改该块的当前版本。另外,Oracle无法简单地跳过这一行,因为这将是不一致读,而且是不可预测的。在这种情况下,我们发现Oracle会从头重新开始写修改。

7.4.1             一致读和当前读

Oracle处理修改语句时会完成两类块获取。它会执行:

q         一致读(Consistent read):“发现”要修改的行时,所完成的获取就是一致读。

q         当前读(Current read):得到块来实际更新所要修改的行时,所完成的获取就是当前读。

使用TKPROF可以很容易地看到这一点。请考虑以下这个很小的单行例子,它从先前的表T读取和更新一行:

ops$tkyte@ORA10GR1> alter session set sql_trace=true;

Session altered.

ops$tkyte@ORA10GR1> select * from t;

X

----------

10001

ops$tkyte@ORA10G> update t t1 set x = x+1;

1 row updated.

ops$tkyte@ORA10G> update t t2 set x = x+1;

1 row updated.

运行TKPROF并查看结果时,可以看到如下的结果(需要注意,我去掉了报告中的ELAPSEDCPUDISK列):

select * from t

call          count                          query                  current               rows

-------              ------                     ------                     ----------               ----------

Parse           1                          0                         0                         0

Execute       1                          0                         0                         0

Fetch            2                          3                         0                         1

-------             ------                    ------                    ----------              ----------

total              4                          3                         0                         1

update t t1 set x = x+1

call                count                  query                 current              rows

-------             ------                    ------                    ----------              ----------

Parse           1                          0                         0                         0

Execute       1                          3                         3                         1

Fetch            0                          0                         0                         0

-------             ------                    ------                    ----------              ----------

total              2                          3                         3                         1

update t t2 set x = x+1

call                count                  query                 current              rows

-------             ------                    ------                    ----------              ----------

Parse           1                          0                         0                         0

Execute       1                          3                         1                         1

Fetch            0                          0                         0                         0

-------             ------                    ------                    ----------              ----------

total              2                          3                         1                         1

因此,在一个正常的查询中,我们会遇到3个查询模式获取(一致模式获取,queryconsistentmode get)。在第一个UPDATE期间,会遇到同样的3个当前模式获取(current mode get)。完成这些当前模式获取是为了分别得到现在的表块(table block),也就是包含待修改行的块;得到一个undo段块(undo segment block)来开始事务;以及一个undo块(undo block)。第二个更新只有一个当前模式获取,因为我们不必再次完成撤销工作,只是要利用一个当前获取来得到包含待更新行的块。既然存在当前模式获取,这就什么发生了某种修改。在Oracle用新信息修改一个块之前,它必须得到这个块的当前副本。

那么,读一致性对修改有什么影响呢?这么说吧,想像一下你正在对某个数据库表执行以下UPDATE语句:

Update t set x = x+1 where y = 5;

我们知道,查询的WHERE Y=5部分(即读一致阶段)会使用一个一致读来处理(TKPROF报告中的查询模式获取)。这个语句开始执行时表中已提交的WHERE Y=5记录集就是它将看到的记录(假设使用READ COMMITED隔离级别,如果隔离级别是SERIALIZABLE,所看到的则是事务开始是存在的WHERE Y=5记录集)。这说明,如果UPDATE语句从开始到结束要花5分钟来进行处理,而有人在此期间向表中增加并提交了一个新记录,其Y列值为5,那么UPDATE看不到这个记录,因为一致读是看不到新记录的。这在预料之中,也是正常的。但问题是,如果两个会话按顺序执行以下语句会发生什么情况呢?

Update t set y = 10 where y = 5;

Update t Set x = x+1 Where y = 5;

7-8展示了这个时间表。

7-8     更新序列

时间          会话1                会话2                                     注释

T1       Update t                                         这会更新与条件匹配的一行

          set y = 10

          where y = 5;

T2                               Update t                 使用一致读,这会找到会话1修改的记录,

                                                                但是无法更新这个记录,因为会话1已经

                                                                将其阻塞。会话2将被阻塞,并等待这一行可用

                                  set x = x+1

                                  where y = 5;

T3       Commit;                                          这会解放会话2;会话2不再阻塞。他终于可以在

                                                                包含这一行(会话1开始更新时Y等于5的那一行)

                                                                的块上完成当前读

因此开始UPDATEY=5的记录不再是Y=5了。UPDATE的一致读部分指出:“你想更新这个记录,因为我们开始时Y5”,但是根据块的当前版本,你会这样想:”噢,不行,我不能更新这一行,因为Y不再是5了,这可能不对。“

如果我们此时只是跳过这个记录,并将其忽略,就会有一个不确定的更新。这可能会破坏数据一致性和完整性。更新的结果(即修改了多少行,以及修改了哪些行)将取决于以何种顺序命中(找到)表中的行以及此时刚好在做什么活动。在两个不同的数据库中,取同样的一个行集,每个数据库都以相同的顺序运行事务,可能会观察到不同的结果,这只是因为这些行在磁盘上的位置不同。

在这种情况下,Oracle会选择重启动更新。如果开始时Y=5的行现在包含值Y=10Oracle会悄悄地回滚更新,并重启动(假设使用的是READ COMMITTED隔离级别)。如果你使用了SERIALIZABLE隔离级别,此时这个事务就会收到一个ORA-08177can’t serialize access错误。采用READ COMMITTED模式,事务回滚你的更新后,数据库会重启动更新(也就是说,修改更新相关的时间点),而且它并非重新更新数据,而是进入SELECT FOR UPDATE模式,并试图为你的会话锁住所有WHERE Y=5的行。一旦完成了这个锁定,它会对这些锁定的数据运行UPDATE,这样可以确保这一次就能完成而不必(再次)重启动。

但是再想想“会发生什么……“,如果重启动更新,并进入SELECT FOR UPDATE模式(与UPDATE一样,同样有读一致块获取(read-consistent block get)和读当前块获取(read current block get)),开始SELECT FOR UPDATEY=5的一行等到你得到它的当前版本时却发现Y=11,会发生什么呢?SELECT FOR UPDATE会重启动,而且这个循环会再来一遍。

这里要解决两个问题,这两个问题对我来说很有意思。第一个问题是,我们能观察到这种情况吗?可以看到具体是如何发生的吗?第二个问题是,出现这种情况有怎么样呢?这对于我们这些开发人员来说到底有什么意义?下面将分别解决这些问题。

1.4.2             查看重启动

查看重启动比你原来想象的要容易。实际上,可以用一个单行表来观察。我们将用下表进行测试:

ops$tkyte@ORA10G> create table t ( x int, y int );

Table created.

 

ops$tkyte@ORA10G> insert into t values ( 1, 1 );

1 row created.

 

ops$tkyte@ORA10G> commit;

Commit complete.

为了观察重启动,只需要一个触发器打印出一些信息。我们会使用一个BEFORE UPDATE FOR EACH ROW触发器打印出行的前映像和作为更新结果的后映像:

ops$tkyte@ORA10G> create or replace trigger t_bufer

2 before update on t for each row

3 begin

4 dbms_output.put_line

5 ( 'old.x = ' || :old.x ||

6 ', old.y = ' || :old.y );

7 dbms_output.put_line

8 ( 'new.x = ' || :new.x ||

9 ', new.y = ' || :new.y );

10 end;

11 /

Trigger created.

下面可以更新这一行:

ops$tkyte@ORA10G> set serveroutput on

ops$tkyte@ORA10G> update t set x = x+1;

old.x = 1, old.y = 1

new.x = 2, new.y = 1

1 row updated.

到此为止,一切都不出所料:触发器每触发一次,我们都可以看到旧值和新值。不过,要注意,此时还没有提交,这一行仍被锁定。在另一个会话中,执行以下更新:

ops$tkyte@ORA10G> set serveroutput on

ops$tkyte@ORA10G> update t set x = x+1 where x > 0;

当然,这会立即阻塞,因为第一个会话将这一行锁住了。如果现在回到第一个会话,并提交,会看到第二个会话中有以下输出(为清楚起见,这里把更新语句再写一遍):

ops$tkyte@ORA10G> update t set x = x+1 where x > 0;

old.x = 1, old.y = 1

new.x = 2, new.y = 1

old.x = 2, old.y = 1

new.x = 3, new.y = 1

1 row created.

可以看到,行触发器看到这一行有两个版本。行触发器会触发两次:一次提供了行原来的版本以及我们想把原来这个版本修改成什么,另一次提供了最后实际更新的行。由于这是一个BEFORE FOR EACH ROW触发器,Oracle看到了记录的读一致版本,以及我们想对它做的修改。不过,Oracle以当前模式获取块,从而在BEFORE FOR EACH ROW触发器触发之后具体执行更新。它会等待触发器触发后再以当前模式得到块,因为触发器可能会修改:NEW值。因此Oracle在触发器执行之前无法修改这个块,而且触发器的执行可能要花很长时间。由于一次只有一个会话能以当前模式持有一个块;所以Oracle需要对处于当前模式下的时间加以限制。

触发器触发后,Oracle以当前模式获取这个块,并注意到用来查找这一行的X列已经修改过。由于使用了X来定位这条记录,而且X已经修改,所以数据库决定重启动查询。注意,尽管X1更新到2,但这并不会使该行不满足条件(X>0);这条UPDATE语句还是会更新这一行。而且,由于X用于定位这一行,而X的一致读值(这里是1)不同于X的当前模式读值(2),所以在重启动查询时,触发器把值X=2(被另一个会话修改之后)看作是:OLD值,而把X=3看作是:NEW值。

由此就可以看出发生了重启动。要用触发器查看实际的情况;否则,重启动一般是“不可检测的“。这并不是说无法看到重启动的其他症状,例如更新多行时,发现某一行会导致重启动,而导致一个很大的UPDATE语句回滚工作,这也可能是一个症状,但是很难明确地指出”这个症状是重启动造成的“。

可以观察一个有意思的情况,即使语句本身不一定导致重启动,触发器本身也可能导致发生重启动。一般来讲,UPDATEDELETE语句的WHERE子句中引用的列能用于确定修改是否需要重启动。Oracle使用这些列完成一个一致读,然后以当前模式获取块时,如果检测到任何列有修改,就会重启动这个语句。一般来讲,不会检查行中的其他列。例如,下面重新运行前面的例子,这里使用WHERE Y>0来查找行:

ops$tkyte@ORA10G> update t set x = x+1 where y > 0;

old.x = 1, old.y = 1

new.x = 2, new.y = 1

old.x = 2, old.y = 1

new.x = 3, new.y = 1

1 row updated.

你开始可能会奇怪,“查看Y值时,Oracle为什么会把触发器触发两次?它会检查整个行吗?“从输出结果可以看到,尽管我们在搜索Y>0,而且根本没有修改Y,但是更新确实重启动了,触发器又触发了两次。不过,倘若重新创建触发器,只打印出它已触发这个事实就行了,而不再引用:OLD:NEW值:

ops$tkyte@ORA10G> create or replace trigger t_bufer

2 before update on t for each row

3 begin

4 dbms_output.put_line( 'fired' );

5 end;

6 /

Trigger created.

 

ops$tkyte@ORA10G> update t set x = x+1;

fired

1 row updated.

再到第二个会话中,运行更新后,可以观察到它会阻塞(当然会这样)。提交阻塞会话(即第一个会话)后,可以看到以下输出:

ops$tkyte@ORA10G> update t set x = x+1 where y > 0;

fired

1 row updated.

这一次触发器只触发了一次,而不是两次。这说明,:NEW:OLD列值在触发器中引用时,也会被Oracle用于完成重启动检查。在触发器中引用:NEW.X:OLD.X时,会比较X的一致读值和当前读值,并发现二者不同。这就会带来一个重启动。从触发器将这一列的引用去掉后,就没有重启动了。

所以,对此的原则是:WHERE子句中查找行所用的列集会与行触发器中引用的列进行比较。行的一致读版本会与行的当前读版本比较,只要有不同,就会重启动修改。

注意   根据这些信息,我们可以进一步理解为什么使用AFTER FOR EACH ROW触发器比使用BEFORE FOR EACH ROW更高效。AFTER触发器不存在这些问题。

下面再来看另一个问题:“我们为什么要关心重启动?“

1.4.3             为什么重启动对我们很重要?

首先应该注意到“我们的触发器触发了两次!“表中只有一行,而且只有一个BEFORE FOR EACH ROW触发器。我们更新了一行,但触发器却触发了两次。

想想看这会有什么潜在的影响。如果你有一个触发器会做一些非事务性的事情,这可能就是一个相当严重的问题。例如,考虑这样一个触发器,它要发出一个更新(电子邮件),电子邮件的正文是“这是数据以前的样子,它已经修改成现在这个样子“。如果从触发器直接发送这个电子邮件,(在Oracle9i中使用UTL_SMTY,或者在Oracle 10g及以上版本中使用UTL_MAIL),用户就会收到两个电子邮件,而且其中一个报告的更新从未实际发生过。

如果在触发器中做任何非事务性的工作,就会受到重启动的影响。考虑以下影响:

q         考虑一个触发器,它维护着一些PL/SQL全局变量,如所处理的个数。重启动的语句回滚时,对PL/SQL变量的修改不会“回滚“。

q         一般认为,以UTL_开头的几乎所有函数(UTL_FILEUTL_HTTPUTL_SMTP等)都会受到语句重启动的影响。语句重启动时,UTL_FILE不会“取消“对所写文件的写操作。

q         作为自治事务一部分的触发器肯定会受到影响。语句重启动并回滚时,自治事务无法回滚。

所有这些后果都要小心处理,要想到对于每一个触发器可能会触发多次,或者甚至对根本未被语句更新的行也会触发。

之所以要当心可能的重启动,还有一个原因,这与性能有关。我们一直在使用单行的例子,但是如果你开始一个很大的批更新,而且它处理了前100,000条记录后重启动了会怎么样?它会回滚前100,000行的修改,以SELECT FOR UPDATE模式重启动,在此之后再完成那100,000行的修改。

你可能注意到了,放上那个简单的审计跟踪触发器后(即读取:NEW:OLD值的触发器),即使除了增加了这些新触发器外什么也没有改变,但性能可能会突然差到你无法解释的地步。你可能在重启动过去从未用过的查询。或者你增加了一个小程序,它只更新某处的一行,确使过去只运行1个小时的批处理突然需要几个小时才能运行完,其原因只是重启动了过去从未发生过工作。

这不是Oracle的一个新特性,从4.0版本起,Oracle数据库就已经有了这个特性,也正是在这个版本中开始引入读一致性。我自己原先就根本没有注意到这是如何工作的,直到2003年的夏天,等我发现了重启动的影响,终于能回答以前困扰我的大量“怎么会发生这种事情?“之类的问题。了解了这一点后,我几乎完全戒除在触发器里使用自治事务,另外开始重新考虑我的一些应用应该如何实现。例如,我不再从触发器直接发送电子邮件;相反,肯定会在我的事务提交之后用DBMS_JOB或新的Oracle 10g调度工具发送电子邮件。这样能是电子邮件的发送是”事务性的“,也就是说,如果导致触发器触发和发送电子邮件的语句重启动了,它完成的回滚就会回滚DBMS_JOB请求。我修改了在触发器里做的几乎所有非事务性工作,使之在事后的作业中完成,从而得到事务一致性。

1.5   小结

在这一章中,我介绍了不是内容,难度也很小,可能会让你不时地挠头。不过,理解这些问题至关重要。例如,如果你不知道语句级重启动,可能就不会明白某些情况怎么会发生。也就是说,你无法解释实验中观察到的一些情况。实际上,如果你不知道有这些重启动,可能会错误地认为错误是环境造成的,或者是最终用户的错误。这是一个不可重复的问题,因为必须以某种特定顺序做许多事情后才能观察到。

我们介绍了SQL标准中定义的隔离级别的含义,并分析了Oracle如何实现隔离级别,另外还将Oracle的实现与其他数据库的实现做了对照。可以看到,在其他实现中(也就是说,采用读锁提供一致数据的实现),并发性和一致性之间存在着巨大的折衷。为了对数据实现高度并发的访问,就必须对一致性答案减低要求。要想得到一致、正确的答案,则需要忍受并发性的下降。我们看到,在Oracle中不是这样的,因为它提供了多版本特性。

7-9对采用读锁的数据库实现与使用的Oracle多版本做了一个比较。

7-9     Oracle与采用读锁定机制的数据库之间事务、并发性和锁定行为的比较

隔离级别                          实现             写阻塞读        读阻塞写     死锁           不正确的         丢失更新      锁升级

                                                                                  敏感读                        查询结果                            或限制

READ UNCOMMITTED  Oracle                                                                                             

READ COMMITTED       Oracle                                                                                             

READ COMMITTED       Oracle                                                                                     *          

REPEATABLE READ        Oracle                                                                                             

SERIALIZABLE                Oracle                                                                                             

SERIALIZABLE                Oracle                                                                                                 

*利用SELECT FOR UPDATE NOWAIT

必须很好地掌握并发控制以及数据库如何实现并发控制。我一直在大唱多版本和读一致性的赞歌,但是与世界上的所有事物一样,它们也是双刃剑。如果你不了解存在多版本特性,以及它是如何工作的,在应用设计中就会犯错误。请考虑第1章中资源调度程序的例子。在一个不支持多版本及相应非阻塞读的数据库中,该程序原来采用的逻辑可以很好地工作。不过,搬到Oracle中实现时,这个逻辑就有麻烦了,它会使数据完整性遭到破坏。除非你知道它如何工作,否则就会写出可能破坏数据的程序,犯这种错误实在太轻而易举了。