oracle 并发与多版本

并发控制 concurrency control

数据库提供的函数集合,允许多个人同时访问和修改数据。

锁(lock)是Oracle管理共享数据库资源并发访问并防止并发数据库事务之间“相互干涉”的核心机制之一。

Oracle使用了多种锁,包括:
1. TX锁:修改数据的事务在执行期间会获得这种锁。
2. TM锁和DDL锁:在你修改一个对象的内容(对于TM锁)或对象本身(对应DDL锁)时,这些锁可以确保对象的结构不被修改。
3. 闩(latch):这是Oracle的内部锁,用来协调对其共享数据结构的访问。

不论是哪一种锁,请求锁时都存在相关的最小开销。

TX锁在性能和基数方面可扩缩性极好。

TM锁和DDL锁要尽可能地采用限制最小的模式。

闩和队列锁(enqueue)都是轻量级的,而且都很快。


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

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

数据库中事务的基本作用是将数据库从一种一致状态转变为另一种一种状态。ISO SQL标准指定了多种事务隔离级别(transaction isolation level),这些隔离级别定义了一个事务对其他事务做出的修改有多“敏感”。越是敏感,数据库在应用执行的各个事务之间必须提供的隔离程度就越高。

事务隔离级别

ANSI/ISO SQL标准定义了4种事务隔离级别,对于相同的事务,采用不同的隔离级别分别有不同的结果。即使输入相同,而且采用同样的方式来完成同样的工作,也可能得到完全不同的答案,这取决于事务的隔离级别。这些隔离级别是根据3个“现象”定义的,以下就是给定隔离级别可能允许或不允许的3种现象:
1. 脏读(dirty read): 能读取未提交的数据。只要打开别人正在读写的一个OS文件,就会脏读。 脏读,将影响数据完整性,外键约束会遭到破坏,会忽略惟一性约束
2. 不可重复读(nonrepeatable read):如果你在T1时间读取某一行,在T2时间重新读取这一行时,这一行可能已经有所修改。也许它已经消失,有可能被更新了,等等。
3. 幻像读(phantom read):如果你在T1时间执行一个查询,而在T2时间再执行这个查询,此时可能已经向数据库中增加了另外的行,这会影响你的结果。
与不可重复读的区别在于:在幻像读中,已经读取的数据不会改变,只是与以前相比,会有更多的数据满足你的查询条件。


隔离级别 脏读 不可重复 幻像读
READ UNCOMMITTED 允许 允许 允许 (读未提交)级别用来得到非阻塞读(non-blocking read)
READ COMMITTED 允许 允许 不能提供一致的结果
REPEATABLE READ 允许 可以保证由查询得到读一致的(read-consistent)结果
SERIALIZABLE
Oracle明确地支持READ COMMITTED(读已提交)和SERIALIZABLE(可串行化)隔离级别
不过,在Oracle中,READ COMMITTED则有得到读一致查询所需的所有属性。另外,Oracle还秉承了READ UNCOMMITTED的“精神”。(有些数据库)提供脏读的目的是为了支持非阻塞读,也就是说,查询不会被同一个数据的更新所阻塞,也不会因为查询而阻塞同一数据的更新。不过,Oracle不需要脏读来达到这个目的,而且也不支持脏读。但在其他数据库中必须实现脏读来提供非阻塞读。
除了4个已定义的SQL隔离级别外,Oracle还提供了另外一个级别,称为 READ ONLY(只读)。READ ONLY事务相对于无法在SQL中完成任何修改的REPEATABLE READ或SERIALIZABLE事务。如果事务使用READ ONLY隔离级别,只能看到事务开始那一刻提交的修改,但是插入、更新和删除不允许采用这种模式。如果使用这种模式,可以得到REPEATABLE READ和SERIALIZABLE级别的隔离性。

READ UNCOMMITTED

READ UNCOMMITTED隔离级别允许脏读。 Oracle没有利用脏读,甚至不允许脏读。READ UNCOMMITTED隔离级别的根本目标是 提供一个基于标准的定义以支持非阻塞读。Oracle会默认地提供非阻塞读,在数据库中很难阻塞一个SELECT查询。每个查询都以一种读一致的方式执行,而不论是SELECT、INSERT、UPDATE、MERGE,还是DELETE。这里把UPDATE语句称为查询,UPDATE语句有两个部分:一个是WHERE子句定义的读部分,另一个是SET子句定义的写部分。UPDATE语句会对数据库进行读写,就像所有DML语句一样。对此只有一个例外:使用VALUES子句的单行INSET是一个特例,因为这种语句没有读部分,而只有写部分。

先创建表:
scott@ORCL>create table accounts
  2  ( account_number number primary key,
  3  account_balance number not null
  4  );

插入数据:
scott@ORCL>insert into accounts values('123',500);
已创建 1 行。
scott@ORCL>insert into accounts values('456',240.25);
已创建 1 行。
scott@ORCL>insert into accounts values('789',100);
已创建 1 行。
scott@ORCL>commit;
提交完成。
数据如下:
scott@ORCL>select * from accounts;

ACCOUNT_NUMBER ACCOUNT_BALANCE
-------------- ---------------
           123             500
           456          240.25
           789             100

scott@ORCL>select sum(account_balance) from accounts;

SUM(ACCOUNT_BALANCE)
--------------------
              840.25
下面,select语句开始执行,读取第1行、第2行等。在查询中的某一点上,一个事务将$400.00从账户123转到账户789。这个事务完成了两个更新,但是并没有提交。现在数据如下:
帐号 账户金额 是否?
1 123 ($500.00) changed to $100.00 X
2 456 $240.25
3 789 ($100.00) changed to $500.00 X
如果我们执行的查询要访问某一块,其中包含表“最后”已锁定的行(第3行),则会注意到,这一行中的数据在开始执行之后有所改变。
为了提供一个一致(正确)的答案,Oracle在这个时刻 会创建该块的一个副本,其中包含查询开始时行的“本来面目”。
它会读取值$100.00,这就是查询开始时该行的值。
Oracle就有效地绕过了已修改的数据,它没有读修改后的值,而是从undo段(回滚段)重新建立原数据
因此可以返回一致而且正确的答案,而无需等待事务提交。

再来看允许脏读的数据库,这些数据库只会返回读取那一刻在账户789中看到的值,在这里就是$500.00。这个查询会把转账的$400重复统计两次。因此,它不仅会返回错误的答案,而且会返回表中根本不存在的一个总计(任何时间点都没有这样一个总计)。

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

READ COMMITTED

事务只能读取数据库中已经提交的数据。这里没有脏读,可能有不可重复读(也就是说,在同一个事务中重复读取同一行可能返回不同的答案)和幻像读(与事务早期相比,查询不光能看到已经提交的行,还可以看到新插入的行)。在数据库应用中, READ COMMITTED可能是最常用的隔离级别了,这也是Oracle数据库的默认模式
根据先前的规则,在使用READ COMMITTED隔离级别的数据库中执行的查询肯定就会有相同的表现,真的是这样吗?这是不对的。
在Oracle中,由于使用多版本和读一致查询,无论是使用READ COMMITTED还是使用READ UNCOMMITTED,从ACCOUNTS查询得到的答案总是一样的。 Oracle会按查询开始时数据的样子对已修改的数据进行重建,恢复其“本来面目”,因此会返回数据库在查询开始时的答案。
下面来看看其他数据库,如果采用READ COMMITTED模式。我们从表所述的那个时间点开始:
1. 现在正处在表的中间。已经读取并合计了前N行。
2. 另一个事务将$400.00从账户123转到账户789。
3. 事务还没有提交,所以包含账户123和789信息的行被锁定。
我们知道Oracle中到达账户789那一行时会发生什么,它会绕过已修改的数据,发现它原本是$100.00,然后完成工作。
如下显示了其他数据库(非Oracle)采用默认的READ COMMITTED模式运行时可能得到的答案:
非Oracle数据库使用READ COMMITTED隔离级别时的时间表
时间 查询 转账事务
T1 读取第1行。到目前为止Sum=$500.00
T2 读取第2行。到目前为止Sum=$740.25
T3 更新第1行,并对第1行加一个排他锁,防止出现其他更新和读取。
第1行现在的值为$100.00

T4 读取第N行。Sum=…
T5 更新第3行,对这一行加一个排他锁。
现在第3行的值是$500.00
T6 试图读取第3行,发现这一行被锁定。
会话会阻塞,并等待这一行重新可用。
对这个查询的所有处理都停止
T7 提交事务
T8 读取第3行,发现值为$500.00,提供一个
最终答案,可惜这里把$400.00重复计入了两次

首先要注意到,在这个数据库中,到达账户789时, 我们的查询会被阻塞。这个会话必须等待这一行,真正持有排它锁的事务提交。这是因为这个原因,所以很多人养成一种坏习惯,在执行每条语句后都立即提交,而 不是处理一个合理的事务,其中包括将数据库从一种一致状态转变为另一种一致状态所需的所有语句。在大多数其他数据库中,更新都会干涉读取。在这种情况下,我们不仅会让用户等待,而且他们苦苦等待的最后结果居然还是不正确的。我们会到数据库中从来没有过的答案,这就像脏读一样,但是与脏 读不同的是,这一次还需要用户等待这个错误的答案。
不同的数据库尽管采用相同的、显然安全的隔离级别,而且在完全相同的环境中执行,仍有可能返回完全不同的答案。要知道的重要的一点是, 在Oracle中,非阻塞读并没有以答案不正确作为代价

REPEATABLE READ

它不仅能给出一致的正确答案,还能避免丢失更新。
1. 得到一致的答案
如果隔离级别是REPEATABLE READ,从给定查询得到的结果相对于某个时间点来说应该是一致的。 大多数数据库(不包括Oracle)都通过使用低级的共享读锁来实现可重复读。共享读锁会防止其他会话修改我们已经读取的数据。当然,这会降低并发性。Oracle则采用了更具并发性的多版本模型来提供读一致的答案。
在Oracle中,通过使用多版本,得到的答案相对于查询开始执行那个时间点是一致的。在其他数据库中,通过使用共享读锁,可以得到相对于查询完成那个时间点一致的答案,也就是说,查询结果相对于我们得到的答案的那一刻是一致的。
在一个采用共享读锁来提供可重复读的系统中,可以观察到,查询处理表中的行时,这些行都会锁定。
非Oracle数据库使用READ REPEATABLE隔离级别时的时间表1
时间 查询 转账事务
T1 读取第1行。到目前为止Sum=$500.00。
块1上有一个共享读锁
T2 读取第2行。到目前为止Sum=$740.25。
块2上有一个共享读锁
T3 试图更新第1行,但是被阻塞。
这个事务被挂起,直至可以得到一个排他锁
T4 读取第N行。Sum=…
T5 读取第3行,看到$100.00,提供最后的答案
T6 提交事务
T7 更新第1行,并对这一块加一个排他锁。
现在第1行有$100.00
T8 更新第3行,对这一块加一个排它锁。
第3行现在的值为$500.00。
提交事务
现在我们得到了正确的答案,但是这是有代价的: 需要物理地阻塞一个事务,并且顺序执行两个事务。这是使用共享读锁来得到一致答案的副作用之一:数据的读取器会阻塞数据的写入器。不仅如此,在这些系统,数据的写入器还会阻塞数据读取器。

由此可以看到,共享读锁会妨碍并发性,而且还导致有欺骗性的错误。在下表中,我们先从原来的表开始,不过这一次的目标是把$50.00从账户789转账到账户123。
非Oracle数据库使用READ REPEATABLE隔离级别时的时间表2
时间 查询 转账事务
T1 读取第1行。到目前为止Sum=$500.00。
块1上有一个共享读锁
T2 读取第2行。到目前为止Sum=$740.25。
块2上有一个共享读锁
T3 更新第3行,对块3加一个排他锁,防止出现其他更新和共享读锁。
现在这一行的值为$50.00
T4 读取第N行。Sum=…
T5 试图更新第1行,但是被阻塞。
这个事务被挂起,直至可以得到一个排他锁
T6 试图读取第3行,但是做不到,
因为该行已经有一个排他锁
我 们陷入了经典的死锁条件。我们的查询拥有更新需要的资源,而更新也持有着查询所需的资源。查询与更新事务陷入死锁。要把其中一个作为牺牲品,将其中止。这 样说来,我们可能会花大量的时间和资源,而最终只是会失败并回滚。这是共享读锁的另一个副作用 :数据的读取器和写入器可能而且经常相互死锁
Oracle中可以得到语句级的读一致性,而不会带来读阻塞写的现象,也不会导致死锁。Oracle从不使用共享读锁,从来不会。Oracle选择了多版本机制,尽管更难实现,但绝对更具并发性。
2. 丢失更新:另一个可移植性问题
在采用共享读锁的数据库中,REPEATABLE READ的一个常见用途是防止丢失更新

在一个采用共享读锁(而不是多版本)的数据库中,如果启用了REPEATABLE READ,则不会发生丢失更新错误。这些数据库中之所以不会发生丢失更新,原因是: 这样选择数据就会在上面加一个锁,数据一旦由一个事务读取,就不能被任何其他事务修改。
尽管听上去使用共享读锁好像不错,但如果读取数据时在所有数据上都加共享读锁,这肯定会严重地限制并发读和修改。所以,尽管在这些数据库中这个隔离级别可以防止丢失更新,但是与此同时,也使得完成并发操作的能力化为乌有!
SEAIALIZABLE
一般认为这是最受限的隔离级别,但是它也提供了最高程度的隔离性。SERIALIZABLE事务在一个环境中操作时,就好像没有别的用户在修改数据库中的数据一样。我们读取的所有行在重新读取时都肯定完全一样,所执行的查询在整个事务期间也总能返回相同的结果。例如,如果执行以下查询:
Select * from T;
Begin dbms_lock.sleep( 60*60*24 ); end;
Select * from T;
从T返回的答案总是相同的,就算是我们睡眠了24小时也一样(或者会得到一个ORA-1555:snapshot too old错误)。这个隔离级别可以确保这两个查询总会返回相同的结果。其他事务的副作用(修改)对查询是不可见的,而不论这个查询运行了多长时间。
Oracle中是这样实现SERIALIZABLE事务的:原本通常在语句级得到的读一致性现在可以扩展到事务级。
注意 前面提到过,Oracle中还有一种称为READ ONLY的隔离级别。它有着SERIALIZABLE隔离级别的所有性质,另外还会限制修改。需要指出,SYS用户(或作为SYSDBA连接的用户)不能有READ ONLY或SERIALIZABLE事务。在这方面,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错误。不过,可以再想想看,冒这个险还是值得的。如果你确实要更新信息,就应该使用SELECT … FOR UPDATE,这会实现串行访问。所以,如果使用SERIALIZABLE隔离级别,只要保证以下几点就能很有成效:
1. 一般没有其他人修改相同的数据
2. 需要事务级读一致性
3. 事务都很短(这有助于保证第一点)
Oracle发现这种方法的可扩缩性很好,足以运行其所有TPC-C(这是一个行业标准OLTP基准)。在许多其他的实现中,你会发现这种隔离性都是利用共享读锁达到的,相应地会带来死锁和阻塞。而在Oracle中,没有任何阻塞,但是如果其他会话修改了我们也想修改的数据,则会得到ORA-08177错误。
要记住,如果隔离级别设置为SERIALIZABEL,事务开始之后,你不会看到数据库中做出任何修改,直到提交事务为止。如果应用试图保证其数据完整性约束,如资源调度程序,就必须在这方面特别小心。我们无法保证一个多用户系统中的完整性约束,因为我们看不到其他未提交会话做出的修改。通过使用SERIALIZABLE。这些未提交的修改还是看不到,但是同样也看不到事务开始后执行的已提交的修改

SERIALIZABLE并不意味着用户执行的所有事务都表现得好像是以一种串行化方式一个接一个地执行。SERIALIZABLE不代表事务有某种串行顺序并且总能得到相同的结果。尽管按照SQL标准来说,这种模式不允许前面所述的几种现象,但不能保证事务总按串行方式顺序执行。
sys@ORCL>create table a ( x int );
表已创建。

sys@ORCL>create table b ( x int );
表已创建。
表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;
现在,一起都完成后,表A和B中都有一个值为0的行。如果事务有某种“串行”顺序,就不可能得到两个都包含0值的表。如果会话1在会话2之前执行,表B就会有一个值为1的行。如果会话2在会话1之前执行,那么表A则有一个值为1的行。不过,按照这里的执行方式,两个表中的行都有值0,不论是哪个事务,执行时就好像是此时数据库中只有它一个事务一样。不管会话1查询多少次表B,计数(count)都是对T1时间数据库中已提交记录的计数。不论会话2查询多少次表A,都会得到与T2时间相同的计数。

READ ONLY

READ ONLY事务与SERIALIZABLE事务很相似,惟一的区别是READ ONLY事务不允许修改,因此不会遭遇ORA-08177错误。 READ ONLY事务的目的是支持报告需求,即相对于某个时间点,报告的内容应该是一致的。在Oracle中,采用这种模式,如果一个报告使用50条SELECT语句来收集数据,所生成的结果相对于某个时间点就是一致的,即事务开始的那个时间点。你可以做到这一点,而无需在任何地方锁定数据。
为达到这个目标,就像对单语句一样,也使用了同样的多版本机制。会根据需要从回滚段重新创建数据,并提供报告开始时数据的原样。不过,READ ONLY事务也不是没有问题。在SERIALIZABLE事务中你可能会遇到ORA-08177错误,而在READ ONLY事务中可能会看到ORA-1555:snapshot too old错误。如果系统上有人正在修改你读取的数据,就会发生这种情况。对这个信息所做的修改(undo信息)将记录在回滚段中。但是回滚段以一种循环方式使用,这与重做日志非常相似。报告运行的时间越长,重建数据所需的undo信息就越有可能已经不在那里了。回滚段会回绕,你需要的那部分回滚段可能已经被另外某个事务占用了。此时,就会得到ORA-1555错误,只能从头再来。
惟一的解决方案就是 为系统适当地确定回滚段的大小。问题在于,回滚段是完成数据库工作的一个关键组件,除非有合适的大小,否则就会遭遇这个错误。如果真的遇到了这个错误,这就说明你没有正确地设置回滚段的大小,需要适当地加以修正。

多版本读一致性的含义

多版本 不仅能提供一致(正确)的答案,还有高度的并发性。
一种会失败的常用数据仓库技术
许多人都喜欢用这样一种常用的数据仓库技术:
(1) 他们使用一个触发器维护源表中的一个LAST_UPDATED列。
(2) 最初要填充数据仓库表时,他们要记住当前的时间,为此会选择源系统上的SYSDATE。例如,假设现在刚好是上午9:00。
(3) 然后他们从事务系统中拉(pull)出所有行,这是一个完整的SELECT * FROM TABLE查询,可以得到最初填充的数据仓库。
(4) 要刷新这个数据仓库,他们要再次记住现在的时间。例如,假设已经过去了1个小时,现在源系统上的时间就是10:00.他们会记住这一点。然后拉出自上午9:00(也就是第一次拉出数据之前的那个时刻)以来修改过的所有记录,并把这些修改合并到数据仓库中。
注意 这种技术可能会在两次连续的刷新中将相同的记录“拉出”两次。由于时钟的粒度所致,这是不可避免的。MERGE操作不会受此影响(即更新数据仓库中现有的记录,或插入一个新记录)。

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

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

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

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

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

生产环境中在一个大负载条件下,一个查询使用的I/O比你在测试或开发系统时观察到的I/O要多得多,而你无法解释这一现象。你查看查询执行的I/O时,注意到它比你在开发系统中看到的I/O次数要多得多。然后,你再在测试环境中恢复这个实例,却发现I/O又 降下来了。但是到了生产环境中,它又变得非常高(但是好像还有些变化:有时高,有时低,有时则处于中间)。可以看到,造成这种现象的原因是:在你测试系统 中,由于它是独立的,所以不必撤销事务修改。不过, 在生产系统中,读一个给定的块时,可能必须撤销(回滚)多个事务所做的修改,而且每个回滚都可能涉及I/O来获取undo信息并应用于系统
可能只是要查询一个表,但是这个表上发生了多个并发修改,因此你看到Oracle正在读undo段,从而将块恢复到查询开始时的样子。
scott@ORCL>create table t ( x int );
表已创建。

scott@ORCL>insert into t values ( 1 );
已创建 1 行。

scott@ORCL>exec dbms_stats.gather_table_stats( user, 'T' );
PL/SQL 过程已成功完成。

scott@ORCL>select * from t;

         X
----------
         1
下面,将会话设置为使用SERIALIZABLE隔离级别,这样无论在会话中运行多少次查询,都将得到事务开始时刻的查询结果:
scott@ORCL>alter session set isolation_level=serializable;
会话已更改。
下面查询这个小表,并观察执行的I/O次数:
scott@ORCL>set autotrace on statistics
scott@ORCL>select * from t;

         X
----------
         1


统计信息
----------------------------------------------------------
          0  recursive calls
          0  db block gets
          7  consistent gets
          0  physical reads
          0  redo size
        524  bytes sent via SQL*Net to client
        520  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed
由此可见,完成这个查询用了7个I/O(一致获取,consistent get)。在另一个会话中,我们将反复修改这个表:
scott@ORCL>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 过程已成功完成。
再返回到前面的SERIALIZABLE会话,重新运行同样的查询:
scott@ORCL>select * from t;

         X
----------
         1


统计信息
----------------------------------------------------------
          0  recursive calls
          0  db block gets
      10002  consistent gets
          0  physical reads
          0  redo size
        524  bytes sent via SQL*Net to client
        520  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed
这一次执行了10,002次I/O,简直有天壤之别。那么,所有这些I/O是从哪里来的呢?这是 因为Oracle回滚了对该数据库块的修改。在运行第二个查询时,Oracle知道查询获取和处理的所有块都必须针对事务开始的那个时刻。到达缓冲区缓存时,我们发现,缓存中的块“太新了”,另一个会话已经把这个块修改了10,000次。查询无法看到这些修改,所以它开始查找undo信息,并撤销上一次所做的修改。它发现这个回滚块还是太新了,然后再对它做一次回滚。这个工作会复发进行,直至最后发现事务开始时的那个版本(即事务开始时数据库中的已提交块)。这才是我们可以使用的块,而且我们用的就是这个块。
注意 如果你想再次运行SELECT * FROM T,可能会看到I/O再次下降到3;不再是10,002。为什么呢?Oracle能把同一个块的多个版本保存在缓冲区缓存中。你撤销对这个块的修改时,也就把相应的版本留在缓存中了,这样以后执行查询时就可以直接访问。
那么,是不是只在使用SERIALIZABLE隔离级别时才会遇到这个问题呢?不,绝对不是。可以考虑一个运行5分钟的查询。在查询运行的这5分钟期间,它从缓冲区缓存获取块。每次从缓冲区缓存获取一个块时,都会完成这样一个检查:“这个块是不是太新了?如果是,就将其回滚。”另外,要记住, 查询运行的时间越长,它需要的块在此期间被修改的可能性就越大。
现在,数据库希望进行这个检查(也就是说,查看块是不是“太新”,并相应地回滚修改)。正是由于这个原因,缓冲区缓存实际上可能在内存中包含同一个块的多个版本。通过这种方式,很有可能你需要的版本就在缓存中,已经准备好,正等着你使用,而无需使用undo信息进行物化。请看以下查询:
scott@ORCL>select file#, block#, count(*)
  2  from v$bh
  3  group by file#, block#
  4  having count(*) > 3
  5  order by 3
  6  /

     FILE#     BLOCK#   COUNT(*)
---------- ---------- ----------
         2       4283          4
         2      30659          5
         2      65412          5
         2      30958          5
         2      65413          6
         4        149          6
         2      65415          6
         4       3725          6
         2      30707          6
         2      65414          6

已选择10行。


统计信息
----------------------------------------------------------
        417  recursive calls
          0  db block gets
         83  consistent gets
          3  physical reads
          0  redo size
        824  bytes sent via SQL*Net to client
        520  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
         11  sorts (memory)
          0  sorts (disk)
         10  rows processed
可以用这个查询查看这些块。一般而言,任何时间点上缓存中一个块的版本大约不超过6个,但是这些版本可以由需要它们的任何查询使用。
通常就是这些小的“热表”会因为读一致性遭遇I/O膨胀问题。另外,如果查询需要针对易失表长时间运行,也经常受到这个问题的影响。运行的时间越长,“它们也就会运行得越久”,因为过一段时间,它们可能必须完成更多的工作才能从缓冲区缓存中获取一个块。

写一致性

到此为止,我们语句了解了读一致性: Oracle可以使用undo信息来提供非阻塞的查询和一致(正确)的读。我们了解到,查询时,Oracle会从缓冲区缓存中读出块,它能保证这个块版本足够“旧”,能够被该查询看到。
但是,这又带来了以下的问题:写/修改会怎么样呢?如果运行以下UPDATE语句,会发生什么:
update t set x = 2 where x = 5;
在该语句运行时,有人将这条语句已经读取的一行从x=5更新为x=6,并提交,如果是这样会发生什么情况?也就是说,在UPDATE开始时,某一行有值x=5。在UPDATE使用一致读来读取表时,它看到了UPDATE开始时这一行是x=5。但是,现在x的当前值是6,不再是5了,在更新X的值之前,Oracle会查看x是否还是5。现在会发生什么呢?这会对更新有什么影响?
显然,我们不能修改块的老版本,修改一行时,必须修改该块的当前版本。另外,Oracle无法简单地跳过这一行,因为这将是不一致读,而且是不可预测的。在这种情况下,我们发现Oracle会从头重新开始写修改。

1一致读和当前读

Oracle处理修改语句时会完成两类块获取。它会执行:
1. 一致读(Consistent read):“发现”要修改的行时,所完成的获取就是一致读。
2. 当前读(Current read):得到块来实际更新所要修改的行时,所完成的获取就是当前读。
使用 TKPROF可以很容易地看到这一点:
scott@ORCL>alter session set sql_trace=true;
会话已更改。
scott@ORCL>alter system set  timed_statistics=true;
系统已更改。
scott@ORCL>show parameter sql_trace
NAME                                 TYPE        VALUE
------------------------------------ ----------- ------------------------------
sql_trace                            boolean     TRUE
scott@ORCL>SELECT VALUE  FROM V$PARAMETER WHERE NAME = 'user_dump_dest' ;
VALUE
----------------------------------------
d:\app\administrator\diag\rdbms\orcl\orcl\trace
scott@ORCL>select * from t; X---------- 10003scott@ORCL>update t t1 set x = x+1;已更新 1 行。scott@ORCL>update t t2 set x = x+1;已更新 1 行。scott@ORCL>select * from t ; X---------- 10005
 运行TKPROF并查看结果时,可以看到如下的结果: 
  
C:\Users\Administrator>tkprof D:\app\Administrator\diag\rdbms\orcl\orcl\trace\orcl_ora_5604.trc D:\trace.txt print=100 record=sql.txt sys=no

TKPROF: Release 11.2.0.1.0 - Development on 星期日 5月 6 14:04:38 2018

Copyright (c) 1982, 2009, Oracle and/or its affiliates.  All rights reserved.
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        3		0		0		0
Execute      3		21		3		3
Fetch        0		0		0		0
------- ------		----------	----------	----------
total        6		21		3		3
因此,在一个正常的查询中,我们会遇到3个查询模式获取(一致模式获取,query(consistent)mode get)。在第一个UPDATE期间,会遇到同样的3个当前模式获取(current mode get)。完成这些当前模式获取是为了分别得到现在的表块(table block),也就是包含待修改行的块;得到一个undo段块(undo segment block)来开始事务;以及一个undo块(undo block)。第二个更新只有一个当前模式获取,因为我们不必再次完成撤销工作,只是要利用一个当前获取来得到包含待更新行的块。既然存在当前模式获取,这就什么发生了某种修改。在Oracle用新信息修改一个块之前,它必须得到这个块的当前副本。
那么,读一致性对修改有什么影响呢?这么说吧,想像一下你正在对某个数据库表执行以下UPDATE语句:
我们知道,查询的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 更新序列
时间 会话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的那一行)
的块上完成当前读
因此开始UPDATE时Y=5的记录不再是Y=5了。UPDATE的一致读部分指出:“你想更新这个记录,因为我们开始时Y是5”,但是根据块的当前版本,你会这样想:”噢,不行,我不能更新这一行,因为Y不再是5了,这可能不对。“
如果我们此时只是跳过这个记录,并将其忽略,就会有一个不确定的更新。这可能会破坏数据一致性和完整性。更新的结果(即修改了多少行,以及修改了哪些行)将 取决于以何种顺序命中(找到)表中的行以及此时刚好在做什么活动。在两个不同的数据库中,取同样的一个行集,每个数据库都以相同的顺序运行事务,可能会观 察到不同的结果,这只是因为这些行在磁盘上的位置不同。
在这种情况下,Oracle会选择 重启动更新。如果开始时Y=5的行现在包含值Y=10,Oracle会悄悄地回滚更新,并重启动(假设使用的是READ COMMITTED隔离级别)。如果你使用了SERIALIZABLE隔离级别,此时这个事务就会收到一个ORA-08177: can’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 UPDATE时Y=5的一行等到你得到它的当前版本时却发现Y=11,会发生什么呢?SELECT FOR UPDATE会重启动,而且这个循环会再来一遍。

查看重启动

scott@ORCL>create table t ( x int, y int );
表已创建。

scott@ORCL>insert into t values ( 1, 1 );
已创建 1 行。

scott@ORCL>commit;
提交完成。
为了观察重启动,只需要一个触发器打印出一些信息。我们会使用一个BEFORE UPDATE FOR EACH ROW触发器打印出行的前映像和作为更新结果的后映像:
scott@ORCL>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  /

触发器已创建
下面可以更新这一行:
scott@ORCL>set serveroutput on
scott@ORCL>update t set x = x+1;
old.x = 1, old.y = 1
new.x = 2, new.y = 1

已更新 1 行。
到此为止,一切都不出所料:触发器每触发一次,我们都可以看到旧值和新值。不过,要注意,此时还没有提交,这一行仍被锁定。在另一个会话中,执行以下更新:
scott@ORCL>set serveroutput on
scott@ORCL>update t set x = x+1 where x > 0;
立即阻塞,因为第一个会话将这一行锁住了。如果现在回到第一个会话,并提交,
scott@ORCL>commit;
会看到第二个会话中有以下输出:
scott@ORCL>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 行。
可以看到,行触发器看到这一行有两个版本。行触发器会触发两次:一次提供了行原来的版本以及我们想把原来这个版本修改成什么,另一次提供了最后实际更新的行。由于这是一个BEFORE FOR EACH ROW触发器,Oracle看到了记录的读一致版本,以及我们想对它做的修改。不过,Oracle以当前模式获取块,从而在BEFORE FOR EACH ROW触发器触发之后具体执行更新。它会等待触发器触发后再以当前模式得到块,因为触发器可能会修改:NEW值。因此Oracle在触发器执行之前无法修改这个块,而且触发器的执行可能要花很长时间。由于一次只有一个会话能以当前模式持有一个块;所以Oracle需要对处于当前模式下的时间加以限制。
触发器触发后,Oracle以当前模式获取这个块,并注意到用来查找这一行的X列已经修改过。由于使用了X来定位这条记录,而且X已经修改,所以数据库决定 重启动查询。注意,尽管X从1更新到2,但这并不会使该行不满足条件(X>0);这条UPDATE语句还是会更新这一行。而且,由于X用于定位这一行,而X的一致读值(这里是1)不同于X的当前模式读值(2),所以在重启动查询时,触发器把值X=2(被另一个会话修改之后)看作是:OLD值,而把X=3看作是:NEW值。
由此就可以看出发生了重启动。要用触发器查看实际的情况;否则,重启动一般是“不可检测的“。例如更新多行时,发现某一行会导致重启动,而导致一个很大的UPDATE语句回滚工作,这也可能是一个症状,但是很难明确地指出”这个症状是重启动造成的“。
即使语句本身不一定导致重启动,触发器本身也可能导致发生重启动。一般来讲,UPDATE或DELETE语句的WHERE子句中引用的列能用于确定修改是否需要重启动。Oracle使用这些列完成一个一致读,然后以当前模式获取块时,如果检测到任何列有修改,就会重启动这个语句。一般来讲,不会检查行中的其他列。例如,下面重新运行前面的例子,这里使用WHERE Y>0来查找行:
scott@ORCL>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 行。
你开始可能会奇怪,“查看Y值时,Oracle为什么会把触发器触发两次?它会检查整个行吗?“从输出结果可以看到,尽管我们在搜索Y>0,而且根本没有修改Y,但是更新确实重启动了,触发器又触发了两次。不过,倘若重新创建触发器,只打印出它已触发这个事实就行了,而不再引用:OLD和:NEW值:
scott@ORCL>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  /

触发器已创建

scott@ORCL>update t set x = x+1;
fired

已更新 1 行。
再到第二个会话中,运行更新后,可以观察到它会阻塞。提交阻塞会话(即第一个会话)后,可以看到以下输出:
scott@ORCL>update t set x = x+1 where y > 0;
fired

已更新 1 行。
这一次触发器只触发了一次,而不是两次。这说明, :NEW和:OLD列值在触发器中引用时,也会被Oracle用于完成重启动检查。在触发器中引用:NEW.X和:OLD.X时,会比较X的一致读值和当前读值,并发现二者不同。这就会带来一个重启动。从触发器将这一列的引用去掉后,就没有重启动了。
所以,对此的原则是:WHERE子句中查找行所用的列集会与行触发器中引用的列进行比较。行的一致读版本会与行的当前读版本比较,只要有不同,就会重启动修改。
注意 使用AFTER FOR EACH ROW触发器比使用BEFORE FOR EACH ROW更高效。AFTER触发器不存在这些问题。

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

首先应该注意到“我们的触发器触发了两次!“表中只有一行,而且只有一个BEFORE FOR EACH ROW触发器。我们更新了一行,但触发器却触发了两次。
想想看这会有什么潜在的影响。如果你有一个触发器会做一些非事务性的事情,这可能就是一个相当严重的问题。例如,考虑这样一个触发器,它要发出一个更新(电 子邮件),电子邮件的正文是“这是数据以前的样子,它已经修改成现在这个样子“。如果从触发器直接发送这个电子邮件,(在Oracle9i中使用UTL_SMTY,或者在Oracle 10g及以上版本中使用UTL_MAIL),用户就会收到两个电子邮件,而且其中一个报告的更新从未实际发生过。
如果在触发器中做任何非事务性的工作,就会受到重启动的影响。考虑以下影响:
1.考虑一个触发器,它维护着一些PL/SQL全局变量,如所处理的个数。重启动的语句回滚时,对PL/SQL变量的修改不会“回滚“。
2.一般认为,以UTL_开头的几乎所有函数(UTL_FILE、UTL_HTTP、UTL_SMTP等)都会受到语句重启动的影响。语句重启动时,UTL_FILE不会“取消“对所写文件的写操作。
3.作为自治事务一部分的触发器肯定会受到影响。语句重启动并回滚时,自治事务无法回滚。
所有这些后果都要小心处理,要想到对于每一个触发器可能会触发多次,或者甚至对根本未被语句更新的行也会触发。
之所以要当心可能的重启动,还有一个原因,这与性能有关。我们一直在使用单行的例子,但是如果你开始一个很大的批更新,而且它处理了前100,000条记录后重启动了会怎么样?它会回滚前100,000行的修改,以SELECT FOR UPDATE模式重启动,在此之后再完成那100,000行的修改。
你可能注意到了,放上那个简单的审计跟踪触发器后(即读取:NEW和:OLD值的触发器),即使除了增加了这些新触发器外什么也没有改变,但性能可能会突然差到你无法解释的地步。你可能在重启动过去从未用过的查询。或者你增加了一个小程序,它只更新某处的一行,确使过去只运行1个小时的批处理突然需要几个小时才能运行完,其原因只是重启动了过去从未发生过工作。

猜你喜欢

转载自blog.csdn.net/a0001aa/article/details/80110803