开发多用户数据库应用,最大的难题之一是:一方面要力争最大的并发访问,而同时还要确保每一用户 能以一致的方式读取和修改数据。力争最大的并发访问需要用锁定机制,而确保一致读和修改数据则需要一些并发控制机制。
1. 并发控制
Oracle对并发的支持不仅使用高效的锁定,还实现了一种多版本体系结构,它提供了一种受控但高度并发的数据访问。这里的多版本指的是可以同时地物化多个版本的数据,这也是Oracle提供读一致性视图的机制。多版本有一个很好的副作用,即数据的读取器(reader)绝对不会被数据的写入器(writer)所阻塞。换句话说,写不会阻塞读。这是Oracle与其他数据库之间的一个根本区别。
默认情况下,Oracle的读一致性多版本视图是应用与语句级的,即对应与每一个查询。也可以改为事务级的。数据库中事务的基本作用是将数据库从一种一致状态转变为另一种一种状态。ISO SQL标准指定了多种事务隔离级别(transaction isolation level),这些隔离级别定义了一个事务对其他事务做出的修改有多“敏感”。越是敏感,数据库在应用执行的各个事务之间必须提供的隔离程度就越高。
2.事务隔离级别
ANSI/ISO SQL标准定义了4种事务隔离级别,对于相同的事务,采用不同的隔离级别分别有不同的结果。也就是说,即使输入相同,而且采用同样的方式来完成同样的工作,也可能得到完全不同的答案,这取决于事务的隔离级别。这些隔离级别是根据3个“现象”定义的,以下就是给定隔离级别可能允许或不允许的3种现象:
a)脏读(dirty read):你能读取未提交的数据,也就是脏数据。只要打开别人正在读写的一个OS文件(不论文件中有什么数据),就可以达到脏读的效果。如果允许脏读,将影响数据完整性,另外外键约束会遭到破坏,而且会忽略惟一性约束。
这里的修改是已经提交了的,与脏读不同。
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会按查询开始时数据的样子对已修改的数据进行重建,恢复其“本来面目”,因此会返回数据库在查询开始时的答案。
自己建了一个测试表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