事务隔离级别

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

更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题--最后的更新覆盖了由其他事务所做的更新。

注意 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 READ或SERIALIZABLE事务。如果事务使用READ ONLY隔离级别,只能看到事务开始那一刻提交的修改,但是插入、更新和删除不允许采用这种模式(其他会话可以更新数据,但是READ ONLY事务不行)。如果使用这种模式,可以得到REPEATABLE READ和SERIALIZABLE级别的隔离性。

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

SERIALIZABLE一般认为这是最受限的隔离级别,但是它也提供了最高程度的隔离性。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 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错误。不过,可以再想想看,冒这个险还是值得的。如果你确实要更新信息,就应该使用第1章所述的SELECT … FOR UPDATE,这会实现串行访问。所以,如果使用SERIALIZABLE隔离级别,只要保证以下几点就能很有成效:
一般没有其他人修改相同的数据
需要事务级读一致性
事务都很短(这有助于保证第一点)
301 / 860
Oracle发现这种方法的可扩缩性很好,足以运行其所有TPC-C(这是一个行业标准OLTP基准:有关详细内容请见www.tpc.org)。在许多其他的实现中,你会发现这种隔离性都是利用共享读锁达到的,相应地会带来死锁和阻塞。而在Oracle中,没有任何阻塞,但是如果其他会话修改了我们也想修改的数据,则会得到ORA-08177错误。不过,与其他系统中得到死锁和阻塞相比,我们得到的错误要少得多。

写一致性
如果两个会话按顺序执行以下语句会发生什么情况呢?
Update t set y = 10 where y = 5;
Update t Set x = x+1 Where y = 5;

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

在这种情况下,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会重启动,而且这个循环会再来一遍。
一致读和当前读
Oracle处理修改语句时会完成两类块获取。它会执行:
? 一致读(Consistent read):发现”要修改的行时,所完成的获取就是一致读。
? 当前读(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并查看结果时,可以看到如下的结果(需要注意,我去掉了报告中的ELAPSED、CPU和DISK列):
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
309 / 860
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个查询模式获取(一致模式获取,query(consistent)mode get)。在第一个UPDATE期间,会遇到同样的3个当前模式获取(current mode get)。完成这些当前模式获取是为了分别得到现在的表块(table block),也就是包含待修改行的块;得到一个undo段块(undo segment block)来开始事务;以及一个undo块(undo block)。第二个更新只有一个当前模式获取,因为我们不必再次完成撤销工作,只是要利用一个当前获取来得到包含待更新行的块。既然存在当前模式获取,这就什么发生了某种修改。在Oracle用新信息修改一个块之前,它必须得到这个块的当前副本。

行触发器看到这一行有两个版本。行触发器会触发两次:一次提供了行原来的版本以及我们想把原来这个版本修改成什么,另一次提供了最后实际更新的行。由于这是一个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值。

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

想 想看这会有什么潜在的影响。如果你有一个触发器会做一些非事务性的事情,这可能就是一个相当严重的问题。例如,考虑这样一个触发器,它要发出一个更新(电 子邮件),电子邮件的正文是“这是数据以前的样子,它已经修改成现在这个样子“。如果从触发器直接发送这个电子邮件,(在Oracle9i中使用UTL_SMTY,或者在Oracle 10g及以上版本中使用UTL_MAIL),用户就会收到两个电子邮件,而且其中一个报告的更新从未实际发生过。
如果在触发器中做任何非事务性的工作,就会受到重启动的影响。考虑以下影响:
? 考虑一个触发器,它维护着一些PL/SQL全局变量,如所处理的个数。重启动的语句回滚时,对PL/SQL变量的修改不会“回滚“。
? 一般认为,以UTL_开头的几乎所有函数(UTL_FILE、UTL_HTTP、UTL_SMTP等)都会受到语句重启动的影响。语句重启动时,UTL_FILE不会“取消“对所写文件的写操作。
? 作为自治事务一部分的触发器肯定会受到影响。语句重启动并回滚时,自治事务无法回滚。
所有这些后果都要小心处理,要想到对于每一个触发器可能会触发多次,或者甚至对根本未被语句更新的行也会触发。
之所以要当心可能的重启动,还有一个原因,这与性能有关

 原子性(atomicity):事务中的所有动作要么都发生,要么都不发生。
 一致性(consistency):事务将数据库从一种一致状态转变为下一种一致状态。
隔离性(isolation):一个事务的影响在该事务提交前对其他事务都不可见。
持久性(durability):事务一旦提交,其结果就是永久性的。

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/8797129/viewspace-693229/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/8797129/viewspace-693229/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值