并发控制 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),这些隔离级别定义了一个事务对其他事务做出的修改有多“敏感”。越是敏感,数据库在应用执行的各个事务之间必须提供的隔离程度就越高。
事务隔离级别
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则有得到读一致查询所需的所有属性。另外,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行),则会注意到,这一行中的数据在开始执行之后有所改变。
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,然后完成工作。
时间 查询 转账事务
T1 读取第1行。到目前为止Sum=$500.00
T2 读取第2行。到目前为止Sum=$740.25
T3 更新第1行,并对第1行加一个排他锁,防止出现其他更新和读取。
第1行现在的值为$100.00
T4 读取第N行。Sum=…
T5