Oracle 的并发与多版本
开发多用户数据库应用,最大的难题之一是:一方面要力争最大的并发访问,而同时还要确保每一用户 能以一致的方式读取和修改数据。
开发多用户数据库应用,最大的难题之一是:一方面要力争最大的并发访问,而同时还要确保每一用户 能以一致的方式读取和修改数据。力争最大的并发访问需要用锁定机制,而确保一致读和修改数据则需要一些并发控制机制。
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就意味着“丢失更新不可能发生”,等你把应用移植到一个没有使用共享读锁作为底层并发控制机制的数据库时,就会痛苦地发现与你预想的并不一样。
尽管听上去使用共享读锁好像不错,但你必须记住,如果读取数据时在所有数据上都加共享读锁,这肯定会严重地限制并发读和修改。所以,尽管在这些数据库中这个隔离级别可以防止丢失更新,但是与此同时,也使得完成并发操作的能力化为乌有!对于这些数据库,你无法鱼和熊掌兼得。