数据库提供的函数集合,允许多个人同时访问和修改数据。
锁(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),这些隔离级别定义了一个事务对其他事务做出的修改有多“敏感”。越是敏感,数据库在应用执行的各个事务之间必须提供的隔离程度就越高。
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。这个事务完成了两个更新,但是并没有提交。现在数据如下:
Select * from T;
Begin dbms_lock.sleep( 60*60*24 ); end;
Select * from T;
从T返回的答案总是相同的,就算是我们睡眠了24小时也一样(或者会得到一个ORA-1555:snapshot too old错误)。这个隔离级别可以确保这两个查询总会返回相同的结果。其他事务的副作用(修改)对查询是不可见的,而不论这个查询运行了多长时间。
ERROR at line 1:
ORA-08177: can't serialize access for this transaction
只要你试图更新某一行,而这一行自事务开始后已经修改,你就会得到这个消息。
sys@ORCL>create table a ( x int );
表已创建。
sys@ORCL>create table b ( x int );
表已创建。
表7-7 SERIALIZABLE事务例子
scott@ORCL>select nvl( min(to_date(start_time,'mm/dd/rr hh24:mi:ss')),sysdate)
2 from v$transaction;
NVL(MIN(TO_DAT
--------------
06-5月 -18
在这个例子中,这就是上午8:59:30,
即修改这一行的事务开始的那个时间。我们在上午10:00刷新数据时,会拉出自那个时间以来发生的所有修改,把这些修改合并到数据仓库中,这就能得到需要的所有东西
scott@ORCL>create table t ( x int );
表已创建。
scott@ORCL>insert into t values ( 1 );
已创建 1 行。
scott@ORCL>exec dbms_stats.gather_table_stats( user, 'T' );
PL/SQL 过程已成功完成。
scott@ORCL>select * from t;
X
----------
1
下面,将会话设置为使用SERIALIZABLE隔离级别,这样无论在会话中运行多少次查询,都将得到事务开始时刻的查询结果:
scott@ORCL>alter session set isolation_level=serializable;
会话已更改。
下面查询这个小表,并观察执行的I/O次数:
scott@ORCL>set autotrace on statistics
scott@ORCL>select * from t;
X
----------
1
统计信息
----------------------------------------------------------
0 recursive calls
0 db block gets
7 consistent gets
0 physical reads
0 redo size
524 bytes sent via SQL*Net to client
520 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
由此可见,完成这个查询用了7个I/O(一致获取,consistent get)。在另一个会话中,我们将反复修改这个表:
scott@ORCL>begin
2 for i in 1 .. 10000
3 loop
4 update t set x = x+1;
5 commit;
6 end loop;
7 end;
8 /
PL/SQL 过程已成功完成。
再返回到前面的SERIALIZABLE会话,重新运行同样的查询:
scott@ORCL>select * from t;
X
----------
1
统计信息
----------------------------------------------------------
0 recursive calls
0 db block gets
10002 consistent gets
0 physical reads
0 redo size
524 bytes sent via SQL*Net to client
520 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
这一次执行了10,002次I/O,简直有天壤之别。那么,所有这些I/O是从哪里来的呢?这是
因为Oracle回滚了对该数据库块的修改。在运行第二个查询时,Oracle知道查询获取和处理的所有块都必须针对事务开始的那个时刻。到达缓冲区缓存时,我们发现,缓存中的块“太新了”,另一个会话已经把这个块修改了10,000次。查询无法看到这些修改,所以它开始查找undo信息,并撤销上一次所做的修改。它发现这个回滚块还是太新了,然后再对它做一次回滚。这个工作会复发进行,直至最后发现事务开始时的那个版本(即事务开始时数据库中的已提交块)。这才是我们可以使用的块,而且我们用的就是这个块。
scott@ORCL>select file#, block#, count(*)
2 from v$bh
3 group by file#, block#
4 having count(*) > 3
5 order by 3
6 /
FILE# BLOCK# COUNT(*)
---------- ---------- ----------
2 4283 4
2 30659 5
2 65412 5
2 30958 5
2 65413 6
4 149 6
2 65415 6
4 3725 6
2 30707 6
2 65414 6
已选择10行。
统计信息
----------------------------------------------------------
417 recursive calls
0 db block gets
83 consistent gets
3 physical reads
0 redo size
824 bytes sent via SQL*Net to client
520 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
11 sorts (memory)
0 sorts (disk)
10 rows processed
可以用这个查询查看这些块。一般而言,任何时间点上缓存中一个块的版本大约不超过6个,但是这些版本可以由需要它们的任何查询使用。
update t set x = 2 where x = 5;
在该语句运行时,有人将这条语句已经读取的一行从x=5更新为x=6,并提交,如果是这样会发生什么情况?也就是说,在UPDATE开始时,某一行有值x=5。在UPDATE使用一致读来读取表时,它看到了UPDATE开始时这一行是x=5。但是,现在x的当前值是6,不再是5了,在更新X的值之前,Oracle会查看x是否还是5。现在会发生什么呢?这会对更新有什么影响?
scott@ORCL>alter session set sql_trace=true; 会话已更改。
scott@ORCL>alter system set timed_statistics=true; 系统已更改。
scott@ORCL>show parameter sql_trace NAME TYPE VALUE ------------------------------------ ----------- ------------------------------ sql_trace boolean TRUE
scott@ORCL>select * from t; X---------- 10003scott@ORCL>update t t1 set x = x+1;已更新 1 行。scott@ORCL>update t t2 set x = x+1;已更新 1 行。scott@ORCL>select * from t ; X---------- 10005scott@ORCL>SELECT VALUE FROM V$PARAMETER WHERE NAME = 'user_dump_dest' ; VALUE ---------------------------------------- d:\app\administrator\diag\rdbms\orcl\orcl\trace
运行TKPROF并查看结果时,可以看到如下的结果:
C:\Users\Administrator>tkprof D:\app\Administrator\diag\rdbms\orcl\orcl\trace\orcl_ora_5604.trc D:\trace.txt print=100 record=sql.txt sys=no TKPROF: Release 11.2.0.1.0 - Development on 星期日 5月 6 14:04:38 2018 Copyright (c) 1982, 2009, Oracle and/or its affiliates. All rights reserved.
因此,在一个正常的查询中,我们会遇到3个查询模式获取(一致模式获取,query(consistent)mode get)。在第一个UPDATE期间,会遇到同样的3个当前模式获取(current mode get)。完成这些当前模式获取是为了分别得到现在的表块(table block),也就是包含待修改行的块;得到一个undo段块(undo segment block)来开始事务;以及一个undo块(undo block)。第二个更新只有一个当前模式获取,因为我们不必再次完成撤销工作,只是要利用一个当前获取来得到包含待更新行的块。既然存在当前模式获取,这就什么发生了某种修改。在Oracle用新信息修改一个块之前,它必须得到这个块的当前副本。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 update t t2 set x = x+1 call count query current rows ------- ------ -------- ---------- ---------- Parse 3 0 0 0 Execute 3 21 3 3 Fetch 0 0 0 0 ------- ------ ---------- ---------- ---------- total 6 21 3 3
那么,读一致性对修改有什么影响呢?这么说吧,想像一下你正在对某个数据库表执行以下UPDATE语句:
我们知道,查询的WHERE Y=5部分(即读一致阶段)会使用一个一致读来处理(TKPROF报告中的查询模式获取)。这个语句开始执行时表中已提交的WHERE Y=5记录集就是它将看到的记录(假设使用READ COMMITED隔离级别,如果隔离级别是SERIALIZABLE,所看到的则是事务开始是存在的WHERE Y=5记录集)。这说明,如果UPDATE语句从开始到结束要花5分钟来进行处理,而有人在此期间向表中增加并提交了一个新记录,其Y列值为5,那么UPDATE看不到这个记录,因为一致读是看不到新记录的。这在预料之中,也是正常的。但问题是,如果两个会话按顺序执行以下语句会发生什么情况呢?
表7-8 更新序列Update t set y = 10 where y = 5; Update t Set x = x+1 Where y = 5;
时间 会话1 会话2 注释
T1 Update t 这会更新与条件匹配的一行
set y = 10
where y = 5;
T2 Update t 使用一致读,这会找到会话1修改的记录,
但是无法更新这个记录,因为会话1已经将其阻塞。
会话2将被阻塞,并等待这一行可用
set x = x+1
where y = 5;
T3 Commit; 这会解放会话2;会话2不再阻塞。他终于可以在
包含这一行(会话1开始更新时Y等于5的那一行)
的块上完成当前读
因此开始UPDATE时Y=5的记录不再是Y=5了。UPDATE的一致读部分指出:“你想更新这个记录,因为我们开始时Y是5”,但是根据块的当前版本,你会这样想:”噢,不行,我不能更新这一行,因为Y不再是5了,这可能不对。“
如果我们此时只是跳过这个记录,并将其忽略,就会有一个不确定的更新。这可能会破坏数据一致性和完整性。更新的结果(即修改了多少行,以及修改了哪些行)将 取决于以何种顺序命中(找到)表中的行以及此时刚好在做什么活动。在两个不同的数据库中,取同样的一个行集,每个数据库都以相同的顺序运行事务,可能会观 察到不同的结果,这只是因为这些行在磁盘上的位置不同。
在这种情况下,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会重启动,而且这个循环会再来一遍。
查看重启动
scott@ORCL>create table t ( x int, y int );
表已创建。
scott@ORCL>insert into t values ( 1, 1 );
已创建 1 行。
scott@ORCL>commit;
提交完成。
为了观察重启动,只需要一个触发器打印出一些信息。我们会使用一个BEFORE UPDATE FOR EACH ROW触发器打印出行的前映像和作为更新结果的后映像:
scott@ORCL>create or replace trigger t_bufer
2 before update on t for each row
3 begin
4 dbms_output.put_line
5 ( 'old.x = ' || :old.x ||
6 ', old.y = ' || :old.y );
7 dbms_output.put_line
8 ( 'new.x = ' || :new.x ||
9 ', new.y = ' || :new.y );
10 end;
11 /
触发器已创建
下面可以更新这一行:
scott@ORCL>set serveroutput on
scott@ORCL>update t set x = x+1;
old.x = 1, old.y = 1
new.x = 2, new.y = 1
已更新 1 行。
到此为止,一切都不出所料:触发器每触发一次,我们都可以看到旧值和新值。不过,要注意,此时还没有提交,这一行仍被锁定。在另一个会话中,执行以下更新:
scott@ORCL>set serveroutput on
scott@ORCL>update t set x = x+1 where x > 0;
立即阻塞,因为第一个会话将这一行锁住了。如果现在回到第一个会话,并提交,
scott@ORCL>commit;
scott@ORCL>update t set x = x+1 where x > 0;
old.x = 1, old.y = 1
new.x = 2, new.y = 1
old.x = 2, old.y = 1
new.x = 3, new.y = 1
已更新 1 行。
可以看到,行触发器看到这一行有两个版本。行触发器会触发两次:一次提供了行原来的版本以及我们想把原来这个版本修改成什么,另一次提供了最后实际更新的行。由于这是一个BEFORE FOR EACH ROW触发器,Oracle看到了记录的读一致版本,以及我们想对它做的修改。不过,Oracle以当前模式获取块,从而在BEFORE FOR EACH ROW触发器触发之后具体执行更新。它会等待触发器触发后再以当前模式得到块,因为触发器可能会修改:NEW值。因此Oracle在触发器执行之前无法修改这个块,而且触发器的执行可能要花很长时间。由于一次只有一个会话能以当前模式持有一个块;所以Oracle需要对处于当前模式下的时间加以限制。
scott@ORCL>update t set x = x+1 where y > 0;
old.x = 1, old.y = 1
new.x = 2, new.y = 1
old.x = 2, old.y = 1
new.x = 3, new.y = 1
已更新 1 行。
你开始可能会奇怪,“查看Y值时,Oracle为什么会把触发器触发两次?它会检查整个行吗?“从输出结果可以看到,尽管我们在搜索Y>0,而且根本没有修改Y,但是更新确实重启动了,触发器又触发了两次。不过,倘若重新创建触发器,只打印出它已触发这个事实就行了,而不再引用:OLD和:NEW值:
scott@ORCL>create or replace trigger t_bufer
2 before update on t for each row
3 begin
4 dbms_output.put_line( 'fired' );
5 end;
6 /
触发器已创建
scott@ORCL>update t set x = x+1;
fired
已更新 1 行。
再到第二个会话中,运行更新后,可以观察到它会阻塞。提交阻塞会话(即第一个会话)后,可以看到以下输出:
scott@ORCL>update t set x = x+1 where y > 0;
fired
已更新 1 行。
这一次触发器只触发了一次,而不是两次。这说明,
:NEW和:OLD列值在触发器中引用时,也会被Oracle用于完成重启动检查。在触发器中引用:NEW.X和:OLD.X时,会比较X的一致读值和当前读值,并发现二者不同。这就会带来一个重启动。从触发器将这一列的引用去掉后,就没有重启动了。