开发多用户数据库应用,最大的难题之一是:一方面要力争最大的并发访问,而同时还要确保每一用户 能以一致的方式读取和修改数据。力争最大的并发访问需要用锁定机制,而确保一致读和修改数据则需要一些并发控制机制。
1、并发控制:
并发控制(concurrency control)是数据库提供的函数集合,允许多个人同时访问和修改数据。锁(lock)是Oracle管理共享数据库资源并发访问并防止并发数据库事务之间“相互干涉”的核心机制之一。总结一下,Oracle使用了多种锁,包括:
TX锁:修改数据的事务在执行期间会获得这种锁。
TM锁和DDL锁:在你修改一个对象的内容(对于TM锁)或对象本身(对应DDL锁)时,这些锁可以确保对象的结构不被修改。
闩(latch):这是Oracle的内部锁,用来协调对其共享数据结构的访问。
Oracle对并发的支持不仅使用高效的锁定,还实现了一种多版本体系结构,它提供了一种受控但高度并发的数据访问。这里的多版本指的是可以同时地物化多个版本的数据,这也是Oracle提供读一致性视图的机制。多版本有一个很好的副作用,即数据的读取器(reader)绝对不会被数据的写入器(writer)所阻塞。换句话说,写不会阻塞读。这是Oracle与其他数据库之间的一个根本区别。
默认情况下,Oracle的读一致性多版本视图是应用与语句级的,即对应与每一个查询。也可以改为事务级的。数据库中事务的基本作用是将数据库从一种一致状态转变为另一种一种状态。ISO SQL标准指定了多种事务隔离级别(transaction isolation level),这些隔离级别定义了一个事务对其他事务做出的修改有多“敏感”。越是敏感,数据库在应用执行的各个事务之间必须提供的隔离程度就越高。
2.事务隔离级别
ANSI/ISO SQL标准定义了4种事务隔离级别,对于相同的事务,采用不同的隔离级别分别有不同的结果。也就是说,即使输入相同,而且采用同样的方式来完成同样的工作,也可能得到完全不同的答案,这取决于事务的隔离级别。这些隔离级别是根据3个“现象”定义的,以下就是给定隔离级别可能允许或不允许的3种现象:
a)脏读(dirty read):你能读取未提交的数据,也就是脏数据。只要打开别人正在读写的一个OS文件(不论文件中有什么数据),就可以达到脏读的效果。如果允许脏读,将影响数据完整性,另外外键约束会遭到破坏,而且会忽略惟一性约束。
b)不可重复读(nonrepeatable read):这意味着,如果你在T1时间读取某一行,在T2时间重新读取这一行时,这一行可能已经有所修改。也许它已经消失,有可能被更新了,等等。
这里的修改是已经提交了的,与脏读不同。
c)幻像读(phantom read):这说明,如果你在T1时间执行一个查询,而在T2时间再执行这个查询,此时可能已经向数据库中增加了另外的行,这会影响你的结果。与不可重复读的区别在于:在幻像读中,已经读取的数据不会改变,只是与以前相比,会有更多的数据满足你的查询条件。
SQL隔离级别是根据这些现象来描述级别的,并没有强制采用某种特定的锁定机制或硬性规定的特定行为,这就允许多种不同的锁定/并发机制存在。
表1 ANSI隔离级别
隔离级别 脏读 不可重复读 幻像读
READ UNCOMMITTED 允许 允许 允许
READ COMMITTED 允许 允许
REPEATABLE READ 允许
SERIALIZABLE
SQL的隔离级别表明Read Committed不能提供一致性的结果,因为有可能产生不可重复读和幻想读,而在Oracle中,Read Committed则有得到读一致查询所需的属性。另外,Oracle还秉承了READ UNCOMMITTED的“精神”。(有些数据库)提供脏读的目的是为了支持非阻塞读,也就是说,查询不会被同一个数据的更新所阻塞,也不会因为查询而阻塞同一数据的更新。不过,Oracle不需要脏读来达到这个目的,而且也不支持脏读。但在其他数据库中必须实现脏读来提供非阻塞读。
除了SQL定义的4个隔离级别外,Oracle还定义了另外一个级别,叫做Read Only。READ ONLY事务相对于无法在SQL中完成任何修改的REPEATABLE READ或SERIALIZABLE事务。如果事务使用READ ONLY隔离级别,只能看到事务开始那一刻提交的修改,但是插入、更新和删除不允许采用这种模式(其他会话可以更新数据,但是READ
ONLY事务不行)。如果使用这种模式,可以得到REPEATABLE READ和SERIALIZABLE级别的隔离性。
以下分别介绍一下这几个隔离级别。
2.1 READ UNCOMMITTED
这个隔离级别允许脏读,但Oracle不利用脏读,甚至不允许脏读。其实Read Uncommitted的根本目标是提供一个基于标准的定义以支持非阻塞读。而Oracle是默认支持非阻塞读的。脏读是不是一个特性,而是一个缺点。Oracle根本不需要脏读,Oracle可以完全得到脏读的所有好处(即无阻塞),而不会带来任何不正确的结果。
它是怎么实现的? 当我们在开始的时候查询一个表中的数据,并修改了这个数据,而在事务的过程中如果有其他事务准备查询这个数据,Oracle会使用多版本创建该块的一个副本,包含原来没修改的值,这样一来,Oracle就有效地绕过了已修改的数据,它没有读修改后的值,而是从undo段,也称为回滚(rollback)重新建立原数据。因此可以返回一致而且正确的答案,而无需等待事务提交。
而那些允许脏读的数据库就会读到修改过的数据。
2.2 READ COMMITTED
READ COMMITTED隔离级别是指,事务只能读取数据库中已经提交的数据。这里没有脏读,不过可能有不可重复读(也就是说,在同一个事务中重复读取同一行可能返回不同的答案)和幻像读(与事务早期相比,查询不光能看到已经提交的行,还可以看到新插入的行)。在数据库应用中,READ COMMITTED可能是最常用的隔离级别了,这也是Oracle数据库的默认模式,很少看到使用其他的隔离级别。
在Oracle中,由于使用多版本和读一致查询,无论是使用READ COMMITTED还是使用READ UNCOMMITTED,对同一表进行查询得到的答案总是一样的。Oracle会按查询开始时数据的样子对已修改的数据进行重建,恢复其“本来面目”,因此会返回数据库在查询开始时的答案。
如果采用其他数据库在Read Committed隔离级别时,别的用户在查询期间如果事务未提交,则别的用户需要等待,直到事务提交,而且最后得到的结果还可能不正确(因为不可重复读)。
自己建了一个测试表t,发现Oracle的事务隔离级别为Read Committed时,不可重复读现象是会产生的。具体做法如下(下一行的时间比上一行后):
会话1: 会话2
create table t(x int);
insert into t values(1);
insert into t values(2);
commit;
delete from t where x=2(开始事务)
update t set x=10 where x=1
select * from t (x=1)
commit;
select * from t (x=10)
commit;
可见Oracle的Read Commited还是会返回不同的结果的。不知道书中为什么说会返回同样的结果。望高手解答!
(这里要得到一致的结果只能设为 SEAIALIZABLE才能得到,而且还要每次transaction 开始的时候设定)
2.3 REPEATABLE READ
REPEATABLE READ的目标是提供这样一个隔离级别,它不仅能给出一致的正确答案,还能避免丢失更新。
一致性读:
如果隔离级别是REPEATABLE READ,从给定查询得到的结果相对于某个时间点来说应该是一致的。大多数数据库(不包括Oracle)都通过使用低级的共享读锁来实现可重复读。共享读锁会防止其他会话修改我们已经读取的数据。当然,这会降低并发性。Oracle则采用了更具并发性的多版本模型来提供读一致的答案。
在Oracle中,通过使用多版本,得到的答案相对于查询开始执行那个时间点是一致的。在其他数据库中,通过使用共享读锁,可以得到相对于查询完成那个时间点一致的答案,也就是说,查询结果相对于我们得到的答案的那一刻是一致的.
但是使用共享读锁来得到一致性的结果有副作用之一:数据的读取器会阻塞数据的写入器。它会影响并发性。还有一个副作用是数据的读取器经常和写入器互相死锁。
可以看到,Oracle中可以得到语句级的读一致性,而不会带来读阻塞写的现象,也不会导致死锁。Oracle从不使用共享读锁,从来不会。Oracle选择了多版本机制,尽管更难实现,但绝对更具并发性。
丢失更新:
在采用共享读锁的数据库中,REPEATABLE READ的一个常见用途是防止丢失更新。在一个采用共享读锁(而不是多版本)的数据库中,如果启用了REPEATABLE READ,则不会发生丢失更新错误。这些数据库中之所以不会发生丢失更新,原因是:这样选择数据就会在上面加一个锁,数据一旦由一个事务读取,就不能被任何其他事务修改。如此说来,如果你的应用认为REPEATABLE
READ就意味着“丢失更新不可能发生”,等你把应用移植到一个没有使用共享读锁作为底层并发控制机制的数据库时,就会痛苦地发现与你预想的并不一样。
尽管听上去使用共享读锁好像不错,但你必须记住,如果读取数据时在所有数据上都加共享读锁,这肯定会严重地限制并发读和修改。所以,尽管在这些数据库中这个隔离级别可以防止丢失更新,但是与此同时,也使得完成并发操作的能力化为乌有!对于这些数据库,你无法鱼和熊掌兼得。
2.4 SEAIALIZABLE
一般认为这是最受限的隔离级别,但是它也提供了最高程度的隔离性。SERIALIZABLE事务在一个环境中操作时,就好像没有别的用户在修改数据库中的数据一样。我们读取的所有行在重新读取时都肯定完全一样,所执行的查询在整个事务期间也总能返回相同的结果。
Oracle采用了一种乐观的方法来实现串行化,它认为你的事务想要更新的数据不会被其他事务所更新,而且把宝押在这上面。一般确实是这样的,所以说通常这个宝是押对了,特别是在事务执行得很快的OLTP型系统中。尽管在其他系统中这个隔离级别通常会降低并发性,但是在Oracle中,倘若你的事务在执行期间没有别人更新你的数据,则能提供同等程度的并发性,就好像没有SERIALIZABLE事务一样。另一方面,这也是有缺点的,如果宝押错了,你就会得到ORA_08177错误。
Oracle试图完全在行级得到这种隔离性,但是即使你想修改的行尚未被别人修改后,也可能得到一个ORA-01877错误。发生ORA-01877错误的原因可能是:包含这一行的块上有其他行正在被修改。
2.5 READ ONLY
READ ONLY事务与SERIALIZABLE事务很相似,惟一的区别是READ ONLY事务不允许修改,因此不会遭遇ORA-08177错误。READ ONLY事务的目的是支持报告需求,即相对于某个时间点,报告的内容应该是一致的。在其他系统中,为此要使用REPEATABLE READ,这就要承受共享读锁的相关影响。在Oracle中,则可以使用READ
ONLY事务。采用这种模式,如果一个报告使用50条SELECT语句来收集数据,所生成的结果相对于某个时间点就是一致的,即事务开始的那个时间点。你可以做到这一点,而无需在任何地方锁定数据。
为达到这个目标,就像对单语句一样,也使用了同样的多版本机制。会根据需要从回滚段重新创建数据,并提供报告开始时数据的原样。不过,READ ONLY事务也不是没有问题。在SERIALIZABLE事务中你可能会遇到ORA-08177错误,而在READ ONLY事务中可能会看到ORA-1555:snapshot too old错误。如果系统上有人正在修改你读取的数据,就会发生这种情况。对这个信息所做的修改(undo信息)将记录在回滚段中。但是回滚段以一种循环方式使用,这与重做日志非常相似。报告运行的时间越长,重建数据所需的undo信息就越有可能已经不在那里了。回滚段会回绕,你需要的那部分回滚段可能已经被另外某个事务占用了。此时,就会得到ORA-1555错误,只能从头再来。
3.多版本读一致性的含义:
我们要理解多版本的机制和含义,才能正确地得到我们想要的结果。例如在常用的数据仓库技术中,我们经常是在某个时刻如上午9点从事务系统拉出数据,作为最初填充数据仓库的数据,然后在过一段时间后,在时刻如上午10点,再从事务系统拉出自9点修改过的所有记录,并把修改合并到数据仓库中。
这一种方式对于其他读会被写阻塞,写会被读阻塞的系统来说,确是可以很好工作。因为在t1时刻如果存在被修改的行,那么我们读的时候会堵塞,直到事务提交,那么9点到10点修改过的所有记录会包括这一行。
但如果采用多版本,同样的一行在9点之前(如9:59:20)已被修改,如果在查询事务来到这一行读取时,她还没有提交,那么读取事务会绕过这个锁,到达undo段里去找到读取事务开始之前那个提交的版本,即使在查询到来时她已经提交了,那我们还是会去undo段里找那个数据,原因是我们只能查询读取事务开始那一瞬间的数据,这就是读一致性的特色。这样一来,我们这次拉数据的时候拉不出来这一行的新数据。但是最重要的是10点再次拉数据的时候也不会拉出这一行更改的数据,为什么呢?因为这次拉数据就是拉的是9点以后修改的数据。
(这是一个网友对此次过程的表述)但如果采用多版本,同样的一行在t1时刻之前已被修改,但未提交,那么还采用这个策略的话,只会拉出t1时刻未修改的数据。然后在t2时刻拉出自t1
时刻修改过的所有记录,但是这一行是t1时刻之前修改的,所以不包含在内。所以我们永远到拉不到那个在t1时刻之前修改的数据。
解决的方式是我们需要用稍微不同的方式来得到“现在”的时间。应该查询V$TRANSACTION,找出最早的当前时间是什么,以及这个视图中START_TIME列记录的时间。我们需要拉出自最老事务开始时间(如果没有活动事务,则取当前的SYSDATE值)以来经过修改的所有记录:
select nvl( min(to_date(start_time,'mm/dd/rr hh24:mi:ss')),sysdate)
from v$transaction;
这样他会得到在t1之前这样被修改的时间,并拉出自那个时间以来修改的数据,最后把修改合并到数据仓库中。
还有一个例子是热表上的I/O超出期望值,这就是生产环境中在一个大负载条件下,一个查询使用的I/O比你在测试或开发系统时观察到的I/O要多得多,而你无法解释这一现象。你查看查询执行的I/O时,注意到它比你在开发系统中看到的I/O次数要多得多,多得简直不可想像。然后,你再在测试环境中恢复这个实例,却发现I/O又 降下来了。但是到了生产环境中,它又变得非常高(但是好像还有些变化:有时高,有时低,有时则处于中间)。可以看到,造成这种现象的原因是:在你测试系统
中,由于它是独立的,所以不必撤销事务修改。不过,在生产系统中,读一个给定的块时,可能必须撤销(回滚)多个事务所做的修改,而且每个回滚都可能涉及I/O来获取undo信息并应用于系统。
4.写一致性:
现在来看看写方面会怎样,例如运行以下语句:
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会从头重新开始写修改。
4.1 一致读和当前读
Oracle处理修改语句时会完成两类块获取。它会执行:
一致读(Consistent read):“发现”要修改的行时,所完成的获取就是一致读。
当前读(Current read):得到块来实际更新所要修改的行时,所完成的获取就是当前读。
如果两个会话按顺序执行以下语句会发生什么情况呢?例如
Update t set y = 10 where y = 5;
Update t Set x = x+1 Where y = 5;
第一条语句在执行后,但没有提交,第二条语句执行就会被堵塞。当第一条语句提交时,第二条开始运行,但这时y已经不是5了,数据库会显示0 row updated.
当第二个语句满足条件时,会进行重启动更新,例如:
create table t(x int, y int);
insert into t values(1,1);
commit;
然后在两个会话中分别执行以下两个语句:
update t set x=x+1;
update t set x=x+1 where x>0;
第一会话执行后,没有提交,第二会话语句执行就会被堵塞。当第一条语句提交后,第二条语句开始运行,数据库会重启动查询,发现x>0还是满足的,x的值会继续被更新到3.
即第二条语句一开始是使用一致性读(consistent read),读到提交的x数据(为1,第一个会话未提交),当实际要更新的时候再使用当前读(current read)去获取数据块的最新版本,发现与原来的值不符合时,就开始重启动update操作。
我的理解是更新的时候,只要发现要更新的块被修改,都会重启动查询。重启动有时会带来麻烦,例如当使用触发器来发送邮件时,用户有可能收到2次邮件。