oracle 锁定 问题

锁(lock)机制用于管理对共享资源的并发访问

数据库中使用锁是为了支持对共享资源进行并发访问,与此同时还能提供数据完整性和一致性


在Oracle中,你会了解到:
事务是每个数据库的核心,它们是“好东西”。
1.应该延迟到适当的时刻才提交。不要太快提交,以避免对系统带来压力。这是因为,如果事务很长或很大,一般不会对系统有压力。相应的原则是:在必要时才提交,但是此前不要提交。事务的大小只应该根据业务逻辑来定。
2.只要需要,就应该尽可能长时间地保持对数据所加的锁。这些锁是你能利用的工具。只要需要,你就应该长期地保持数据上的锁。锁可能并不稀少,而且它们可以防止其他会话修改信息。
3.在Oracle中,行级锁没有相关的开销,根本没有。不论你是有1个行锁,还是1 000 000个行锁,专用于锁定这个信息的“资源”数都是一样的。当然,与修改1行相比,修改1 000 000行要做的工作肯定多得多,但是对1 000 000行锁定所需的资源数与对1行锁定所需的资源数完全相同,这是一个固定的常量。
4.不要以为锁升级“对系统更好”(例如,使用表锁而不是行锁)。在Oracle中,锁升级(lock escalate)对系统没有任何好处,不会节省任何资源。也许有时会使用表锁,如批处理中,此时你很清楚会更新整个表,而且不希望其他会话锁定表中的行。
5.可以同时得到并发性和一致性。每次你都能快速而准确地得到数据。数据读取器不会被数据写入器阻塞。数据写入器也不会被数据读取器阻塞


锁定问题

丢失更新 lost update
是一个经典的数据库问题。实际上,所有多用户计算机环境都存在这个问题。简单地说,出现下面的情况时(按以下所列的顺序),就会发生丢失更新:
(1) 会话Session1中的一个事务获取(查询)一行数据,放入本地内存,并显示给一个最终用户User1。
(2) 会话Session2中的另一个事务也获取这一行,但是将数据显示给另一个最终用户User2。
(3) User1使用应用修改了这一行,让应用更新数据库并提交。会话Session1的事务现在已经执行。
(4) User2也修改这一行,让应用更新数据库并提交。会话Session2的事务现在已经执行。


悲观锁定 pessimistic locking

仅用于有状态(stateful)或有连接(connected)环境,也就是说,你的应用与数据库有一条连续的连接,而且至少在事务生存期中只有你一个人使用这条连接。每个应用都得到数据库的一条直接连接,这条连接只能由该应用实例使用。

假设你在使用一条有状态连接,应用可以查询数据而不做任何锁定:

扫描二维码关注公众号,回复: 184313 查看本文章
scott@ORCL>select empno, ename, sal from emp where deptno = 10;

     EMPNO ENAME             SAL
---------- ---------- ----------
      7782 CLARK            2450
      7839 KING             5000
      7934 MILLER           1300

假设用户选择更新MILLER行。在这个时间点上(即用户还没有在屏幕上做任何修改,但是行已经从数据库中读出一段时间了),应用会绑定用户选择的值,从而查询数据库,并确保数据尚未修改。在SQL*Plus中,为了模拟应用可能执行的绑定调用,可以发出以下命令:

scott@ORCL>variable empno number
scott@ORCL>variable ename varchar2(20)
scott@ORCL>variable sal number
scott@ORCL>exec :empno := 7934;:ename := 'MILLER'; :sal := 1300;

PL/SQL 过程已成功完成。

下面,除了简单地查询值并验证数据尚未修改外,我们要使用 FOR UPDATE NOWAIT锁定这一行。应用要执行以下查询:

scott@ORCL>select empno, ename, sal
  2  from emp
  3  where empno = :empno
  4  and ename = :ename
  5  and sal = :sal
  6  for update nowait
  7  /

     EMPNO ENAME             SAL
---------- ---------- ----------
      7934 MILLER           1300
根据屏幕上输入的数据,应用将提供绑定变量的值,然后重新从数据库查询这一行,这一次会锁定这一行,不允许其他会话更新;因此,这种方法称为悲观锁定

所有表都应该有一个主键(前面的SELECT最多会获取一个记录,因为它包括主键EMPNO),而且主键应该是不可变的(不应更新主键),从这句话可以得出三个结论:
1. 如果底层数据没有改变,就会再次得到MILLER行,而且这一行会被锁定,不允许其他会话更新(但是允许其他会话读)。
2.如果另一个用户正在更新这一行,我们就会得到一个ORA-00054:resource busy(ORA-00054:资源忙)错误。相应地,必须等待更新这一行的用户执行工作。
3. 在选择数据和指定有意更新之间,如果有人已经修改了这一行,我们就会得到0行。这说明,屏幕上的数据是过时的。为了避免前面所述的丢失更新情况,应用需要重新查询(requery),并在允许在最终用户修改之前锁定数据。有了悲观锁定,User2试图更新电话号码字段时,应用现在会识别出地址字段已经修改,所以会重新查询数据。因此,User2不会用这个字段的旧数据覆盖User1的修改。


一旦成功地锁定了这一行,应用就会绑定新值,发出更新命令后,提交所做的修改:

scott@ORCL>update emp
  2  set ename = :ename, sal = :sal
  3  where empno = :empno;

已更新 1 行。

scott@ORCL>commit;

提交完成。
现在可以非常安全地修改这一行。我们不可能覆盖其他人所做的修改,因为已经验证了在最初读出数据之后以及对数据锁定之前数据没有改变。

乐观锁定 optimistic locking

即把所有锁定都延迟到即将执行更新之前才做。换句话说,我们会修改屏幕上的信息而不要锁。我们很乐观,认为数据不会被其他用户修改;因此,会等到最后一刻才去看我们的想法对不对。
这种锁定方法在所有环境下都行得通,但是采用这种方法的话,执行更新的用户“失败”的可能性会加大。这说明,这个用户要更新他的数据行时,发现数据已经修改过,所以他必须从头再来。

可以在应用中同时保留旧值和新值,然后在更新数据时使用如下的更新语句,这是乐观锁定的一种流行实现:

Update table
Set column1 = :new_column1, column2 = :new_column2, ....
Where primary_key = :primary_key
And column1 = :old_column1
And column2 = :old_column2
...

在 此,我们乐观地认为数据没有修改。在这种情况下,如果更新语句更新了一行,那我们很幸运;这说明,在读数据和提交更新之间,数据没有改变。但是如果更新了 零行,我们就会失败;另外一个人已经修改了数据,现在我们必须确定应用中下一步要做什么。是让最终用户查询这一行现在的新值,然后再重新开始事务呢?还是应该根据业务规则解决更新冲突,试图合并两个更新的值?

实际上,前面的UPDATE能避免丢失更新,但是确实有可能被阻塞,在等待另一个会话执行对这一行的UPDATE时,它会挂起。如果所有应用(会话)都使用乐观锁定,那么使用直接的UPDATE一般没什么问题,因为执行更新并提交时,行只会被锁定很短的时间。不过,如果某些应用使用了悲观锁定,它会在一段相对较长的时间内持有行上的锁,你可能就会考虑使用SELECT FOR UPDATE NOWAIT,以此来验证行是否未被修改,并在即将UPDATE之前锁定来避免被另一个会话阻塞。

实现乐观并发控制的方法有很多种。上述方法,即应用本身会存储行的所有“前”(before)映像。下面,我们将介绍另外三种方法,分别是:
1.使用一个特殊的列,这个列由一个数据库触发器或应用程序代码维护,可以告诉我们记录的“版本”
2.使用一个校验和或散列值,这是使用原来的数据计算得出的
3.使用新增的Oracle 10g 特性ORA_ROWSCN。

1. 使用版本列的乐观锁定

如果想保护数据库表不出现丢失更新问题,应对每个要保护的表增加一列这一列一般是NUMBER或DATE/TIMESTAMP列,通常通过表上的一个行触发器来维护。每次修改行时,这个触发器要负责递增NUMBER列中的值,或者更新DATE/TIMESTAMP列。

如果应用要实现乐观并发控制,只需要保存这个附加列的值,而不需要保存其他列的所有“前”映像。应用只需验证请求更新那一刻,数据库中这一列的值与最初读出的值是否匹配。如果两个值相等,就说明这一行未被更新过。

下面使用SCOTT.DEPT表的一个副本来看看乐观锁定的实现:

system@ORCL>create table dept
  2  (
  3     deptno number(2),
  4     dname varchar2(14),
  5     loc varchar2(13),
  6     last_mod timestamp with time zone
  7     default systimestamp
  8     not null,
  9     constraint dept_pk primary key(deptno)
 10  )
 11  /

表已创建。
然后向这个表INSERT(插入)DEPT数据的一个副本:

system@ORCL>insert into dept( deptno, dname, loc )
  2  select deptno, dname, loc
  3  from scott.dept;

已创建4行。

system@ORCL>commit;
提交完成。

以上代码会重建DEPT表,但是将有一个附加的LAST_MOD列,这个列使用TIMESTAMP WITH TIME ZONE数据类型。我们将这个列定义为NOT NULL,以保证这个列必须填有数据,其默认值是当前的系统时间。

这个TIMESTAMP数据类型在Oracle中精度最高,通常可以精确到微秒(百万分之一秒)。如果应用要考虑到用户的思考时间,这种TIMESTAMP级的精度实在是绰绰有余,而且数据库获取一行后,人看到这一行,然后修改,再向数据库发回更新,一般不太可能在不到1秒钟的片刻时间内执行整个过程。

接下来,需要一种方法来维护这个值。我们有两种选择:可以由应用维护这一列,更新记录时将LAST_MOD列的值设置为SYSTIMESTAMP;也可以由触发器/存储过程来维护。如果让应用维护LAST_MOD,这比基于触发器的方法表现更好,因为触发器会代表Oracle对修改增加额外的处理。不过这并不是说:无论什么情况,你都要依赖所有应用在表中经过修改的所有位置上一致地维护LAST_MOD。所以,如果要由各个应用负责维护这个字段,就需要一致地验证LAST_MOD列未被修改,并把LAST_MOD列设置为当前的SYSTIMESTAMP。例如,如果应用查询DEPTNO=10这一行:

system@ORCL>variable deptno number
system@ORCL>variable dname varchar2(14)
system@ORCL>variable loc varchar2(13)
system@ORCL>variable last_mod varchar2(50)
system@ORCL>begin
  2  :deptno := 10;
  3  select dname, loc, last_mod
  4  into :dname,:loc,:last_mod
  5  from dept
  6  where deptno = :deptno;
  7  end;
  8  /

PL/SQL 过程已成功完成。
目前我们看到的是:

system@ORCL>select :deptno dno, :dname dname, :loc loc, :last_mod lm from dual;

       DNO DNAME                            LOC					LM

---------- -------------------------------- ----------------------------	----------------------------------------------------------------

        10 ACCOUNTING                       NEW YORK				18-4月 -18 02.31.19.424000 下午 +08:00

再使用下面的更新语句来修改信息。最后一行执行了一个非常重要的检查,以确保时间戳没有改变,并使用内置函数 TO_TIMESTAMP_TZ(TZ是TimeZone的缩写,即时区) 将以上select(选择)得到的串转换为适当的数据类型。另外,如果发现行已经更新,以下更新语句中的第3行会把LAST_MOD列更新为当前时间:
system@ORCL>update dept
  2  set dname = initcap(:dname),
  3  last_mod = systimestamp
  4  where deptno = :deptno
  5  and last_mod = to_timestamp_tz(:last_mod);

已更新 1 行。

可以看到,这里更新了一行。在此按主键(DEPTNO)更新了这一行,并验证从最初读取记录到执行更新这段时间,LAST_MOD列未被其他会话修改。如果我们想尝试再更新这个记录,仍然使用同样的逻辑,不过没有获取新的LAST_MOD值,就会观察到以下情况:
system@ORCL>update dept
  2  set dname = upper(:dname),
  3  last_mod = systimestamp
  4  where deptno = :deptno
  5  and last_mod = to_timestamp_tz(:last_mod);

已更新0行。
这一次报告称“0 rows updated”(更新了0行),因为关于LAST_MOD的谓词条件不能满足。尽管DEPTNO 10还存在,但是想要执行更新的那个时刻的LAST_MOD值与查询行时的时间戳值不再匹配。所以,应用知道,既然未能修改行,就说明数据库中的数据已经(被别人)改变,现在它必须得出下一步要对此做什么。
不能总是依赖各个应用来维护这个字段,建议 把更新逻辑封装到一个存储过程中,而不要让应用直接更新表。存储过程可以取以上更新中使用的绑定变量作为输入,执行同样的更新。当检测到更新了0行时,存储过程会向客户返回一个异常,让客户知道更新实际上失败了。

还有一种实现是使用一个触发器来维护这个LAST_MOD字段,但是对于这么简单的工作,建议还是避免使用触发器,而让DML来负责。触发器会引入大量开销,而且在这种情况下没有必要使用它们。

2. 使用校验和的乐观锁定

这与前面的版本列方法很相似,不过在此要使用基数据本身来计算一个“虚拟的”版本列
单向散列函数取一个变长输入串(即数据),并把它转换为一个定长的输出串(通常更小),这个输出称为散列值(hash value)。散列值充当输入数据的一个惟一标识符(就像指纹一样)。可以使用散列值来验证数据是否被修改。
需要注意,单向散列函数只能在一个方向上应用。从输入数据计算散列值很容易,但是要生成能散列为某个特定值的数据却很难。
如下方法来计算散列或校验和。所有这些方法都利用了Oracle提供的数据库包:
1. OWA_OPT_LOCK.CHECKSUM:给定一个串,其中一个函数会返回一个16位的校验和。给定ROWID时,另一个函数会计算该行的16位校验和,而且同时将这一行锁定。
2. DBMS_OBFUSCATION_TOOLKIT.MD5:它会计算一个128位的消息摘要。
3. DBMS_CRYPTO.HASH:它能计算一个SHA-1(安全散列算法1,Secure Hash Algorithm 1)或MD4/MD5消息摘要。建议你使用SHA-1算法。
下面在某个应用中查询并显示部门10的信息。查询信息之后,紧接着我们使用DBMS_CRYPTO包计算散列。这是应用中要保留的“版本”信息:
scott@ORCL>begin
  2     for x in ( select deptno, dname, loc
  3     from dept
  4     where deptno = 10 )
  5     loop
  6             dbms_output.put_line( 'Dname: ' || x.dname );
  7             dbms_output.put_line( 'Loc: ' || x.loc );
  8             dbms_output.put_line( 'Hash: ' ||
  9             dbms_crypto.hash
 10             ( utl_raw.cast_to_raw(x.deptno||'/'||x.dname||'/'||x.loc),
 11             dbms_crypto.hash_sh1 ) );
 12     end loop;
 13  end;
 14  /
Dname: ACCOUNTING
Loc: NEW YORK
Hash: C44F7052661CE945D385D5C3F911E70FA99407A6

PL/SQL 过程已成功完成。
可以看到,散列值就是一个很大的16进制位串。DBMS_CRYPTO的返回值是一个RAW变量,显示时,它会隐式地转换为HEX。这个值会在更新前使用。为了执行更新,需要在数据库中获取这一行,并按其现在的样子锁定,然后计算所获取的行的散列值,将这个新散列值与从数据库读出数据时计算的散列值进行比较。上述逻辑表示如下(在实际中,可能使用绑定变量而不是散列值直接量):
scott@ORCL>begin
  2     for x in (
  3             select deptno, dname, loc
  4             from dept
  5             where deptno = 10
  6             for update nowait )
  7     loop
  8             if ( hextoraw( 'C44F7052661CE945D385D5C3F911E70FA99407A6' ) <>
  9                     dbms_crypto.hash
 10                     ( utl_raw.cast_to_raw(x.deptno||'/'||x.dname||'/'||x.loc
),
 11                     dbms_crypto.hash_sh1 ) )
 12             then
 13                     raise_application_error(-20001, 'Row was modified' );
 14             end if;
 15     end loop;
 16     update dept
 17     set dname = lower(dname)
 18     where deptno = 10;
 19     commit;
 20  end;
 21  /

PL/SQL 过程已成功完成。
更新后,重新查询数据,并再次计算散列值,此时可以看到散列值大不相同。如果有人抢在我们前面先修改了这一行,我们的散列值比较就不会成功:
scott@ORCL>begin
  2     for x in (
  3             select deptno, dname, loc
  4             from dept
  5             where deptno = 10 )
  6     loop
  7             dbms_output.put_line( 'Dname: ' || x.dname );
  8             dbms_output.put_line( 'Loc: ' || x.loc );
  9             dbms_output.put_line( 'Hash: ' ||
 10             dbms_crypto.hash
 11                     ( utl_raw.cast_to_raw(x.deptno||'/'||x.dname||'/'||x.loc
),
 12                     dbms_crypto.hash_sh1 ) );
 13     end loop;
 14  end;
 15  /
Dname: accounting
Loc: NEW YORK
Hash: F3DE485922D44DF598C2CEBC34C27DD2216FB90F

PL/SQL 过程已成功完成。
这个例子显示了如何利用散列或校验和来实现乐观锁定。要记住, 计算散列或校验和是一个CPU密集型操作(相当占用CPU),其计算代价很昂贵。如果系统上CPU是稀有资源,在这种系统上就必须充分考虑到这一点。不过,如果从“网络友好性”角度看,这种方法会比较好,因为只需在网络上传输相当小的散列值,而不是行的完整的前映像和后映像(以便逐列地进行比较),所以消耗的资源会少得多。

3. 使用 ORA_ROWSCN的乐观锁定

ORA_ROWSCN函数 它的工作与前面所述的版本列技术很相似,但是可以由Oracle自动执行,而不需要在表中增加额外的列,也不需要额外的更新/维护代码来更新这个值。
ORA_ROWSCN建立在内部Oracle系统时钟(SCN)基础上。在Oracle中,每次提交时,SCN都会推进(其他情况也可能导致SCN推进,要注意,SCN只会推进,绝对不会后退)。这个概念与前面在获取数据时得到ORA_ROWSCN的方法是一样的,更新数据时要验证SCN未修改过。除非你创建表时支持在行级维护ORA_ROWSCN,否则Oracle会在块级维护。也就是说,默认情况下,一个块上的多行会共享相同的ORA_ROWSCN值。如果更新一个块上的某一行,而且这个块上还有另外50行,那么这些行的ORA_ROWSCN也会推进。这往往会导致许多假警报,你认为某一行已经修改,但实际上它并没有改动。因此,需要注意这一点,并了解如何改变这种行为。
sys@ORCL>create table dept
  2     ( deptno, dname, loc, data,
  3     constraint dept_pk primary key(deptno) )
  4  as
  5     select deptno, dname, loc, rpad('*',3500,'*')
  6     from scott.dept;

表已创建。
现在可以观察到每一行分别在哪个块上(假设它们都在同一个文件中,所以如果块号相同,就说明它们在同一个块上):
sys@ORCL>select deptno, dname,
  2  dbms_rowid.rowid_block_number(rowid) blockno,
  3  ora_rowscn
  4  from dept;

    DEPTNO DNAME             BLOCKNO ORA_ROWSCN
---------- -------------- ---------- ----------
        10 accounting          89393    3598647
        20 RESEARCH            89393    3598647
        30 SALES               89394    3598647
        40 OPERATIONS          89394    3598647
结果显示 每块有两行。下面来更新块  89393 上DEPTNO = 10的那一行:
sys@ORCL>update dept
  2  set dname = upper(dname)
  3  where deptno = 10;

已更新 1 行。

sys@ORCL>commit;
提交完成。
接下来观察到,ORA_ROWSCN的结果在块级维护。我们只修改了一行,也只提交了这一行的修改,但是块 89393  上两行的ORA_ROWSCN值都推进了:
sys@ORCL>select deptno, dname,
  2  dbms_rowid.rowid_block_number(rowid) blockno,
  3  ora_rowscn
  4  from dept;

    DEPTNO DNAME             BLOCKNO ORA_ROWSCN
---------- -------------- ---------- ----------
        10 ACCOUNTING          89393    3598829
        20 RESEARCH            89393    3598829
        30 SALES               89394    3598647
        40 OPERATIONS          89394    3598647
如果有人读取DEPTNO=20这一行,看起来这一行已经修改了,但实际上并非如此。块 89394 上的行是“安全”的,我们没有修改这些行,所以它们没有推进。不过,如果更新其中任何一行,两行都将推进。所以现在的问题是:如何修改这种默认行为。遗憾的是,我们必须启用ROWDEPENDENCIES再重新创建这个段。
Oracle9i为数据库增加了行依赖性跟踪 ,可以支持推进复制,以便更好地并行传播修改。在Oracle 10g之前,这个特性只能在复制环境中使用;但是从Oracle 10g开始,还可以利用这个特性用ORA_ROWSCN来实现一种有效的乐观锁定技术。它会为每行增加6字节的开销(所以与自己增加版本列的方法(即DIY版本列方法)相比,并不会节省空间),而实际上,也正是因为这个原因,所以需要重新创建表,而不只是简单地ALTER TABLE:必须修改物理块结构来适应这个特性
下面重新建立我们的表,启用ROWDEPENDENCIES
sys@ORCL>drop table dept;
表已删除。

sys@ORCL>create table dept
  2     ( deptno, dname, loc, data,
  3     constraint dept_pk primary key(deptno) )
  4  ROWDEPENDENCIES
  5  as
  6     select deptno, dname, loc, rpad('*',3500,'*')
  7     from scott.dept;

表已创建。

sys@ORCL>select deptno, dname,
  2  dbms_rowid.rowid_block_number(rowid) blockno,
  3  ora_rowscn
  4  from dept;

    DEPTNO DNAME             BLOCKNO ORA_ROWSCN
---------- -------------- ---------- ----------
        10 accounting          89393    3599113
        20 RESEARCH            89393    3599113
        30 SALES               89394    3599113
        40 OPERATIONS          89394    3599113
又回到前面:两个块上有4行,它们都有相同的ORA_ROWSCN值。现在,更新DEPTNO=10的那一行时:
sys@ORCL>update dept
  2  set dname = upper(dname)
  3  where deptno = 10;

已更新 1 行。

sys@ORCL>commit;
提交完成。
查询DEPT表时应该能观察到以下结果:
sys@ORCL>select deptno, dname,
  2  dbms_rowid.rowid_block_number(rowid) blockno,
  3  ora_rowscn
  4  from dept;

    DEPTNO DNAME             BLOCKNO ORA_ROWSCN
---------- -------------- ---------- ----------
        10 ACCOUNTING          89393    3599206
        20 RESEARCH            89393    3599113
        30 SALES               89394    3599113
        40 OPERATIONS          89394    3599113
此时,只有DEPTNO = 10这一行的ORA_ROWSCN改变。现在可以依靠ORA_ROWSCN 来为我们检测行级修改了。
将SCN转换为墙上时钟时间
使用透明的ORA_ROWSCN列还有一个好处:可以把SCN转换为近似的墙上时钟时间(有+/–3秒的偏差),从而发现行最后一次修改发生在什么时间。例如,可以执行以下查询:
sys@ORCL>select deptno, ora_rowscn, scn_to_timestamp(ora_rowscn) ts
  2  from dept;

    DEPTNO ORA_ROWSCN TS
---------- ---------- ---------------------------------------------------
-----------------
        10    3599206 19-4月 -18 04.56.52.000000000 下午
        20    3599113 19-4月 -18 04.54.24.000000000 下午
        30    3599113 19-4月 -18 04.54.24.000000000 下午
        40    3599113 19-4月 -18 04.54.24.000000000 下午

在此可以看到,在表的最初创建和更新DEPTNO = 10行之间,我等了大约5分钟。不过,从SCN到墙上时钟时间的这种转换有一些限制:数据库的正常运行时间只有5天左右。例如,如果查看一个“旧”表,查找其中最旧的ORA_ROWSCN(注意,在此我作为SCOTT登录;没有使用前面的新表):

select min(ora_rowscn) from dept;
如果我试图把这个SCN转换为一个时间戳,可能看到以下结果:
scott@ORCL>select scn_to_timestamp(min(ora_rowscn)) from dept;

SCN_TO_TIMESTAMP(MIN(ORA_ROWSCN))
---------------------------------------------------------------------------
19-4月 -18 11.01.03.000000000 上午

乐观锁定还是悲观锁定?
悲观锁定在Oracle中工作得非常好,而且与乐观锁定相比,悲观锁定有很多优点。不过,它需要与数据库有一条有状态的连接,如客户/服务器连接,因为无法跨连接持有锁。正是因为这一点,在当前的许多情况下,悲观锁定不太现实。过去,客户/服务器应用可能只有数十个或数百个用户,对于这些应用,悲观锁定是我的不二选择。不过,如今对大多数应用来说,我都建议采用乐观并发控制。要在整个事务期间保持连接,
使用版本列方法,并增加一个
时间戳列(而不只是一个NUMBER)。从长远看,这样能为我提供一个额外的信息:“这一行最后一次更新发生在什么时间?”所以意义更大。而且与散列或校验和方法相比,计算的代价不那么昂贵,在处理LONG、LONG RAW、CLOB、BLOB和其他非常大的列时,散列或校验和方法可能会遇到一些问题,而版本列方法则没有这些问题。
如果必须向一个表增加乐观并发控制,而此时还在利用悲观锁定机制使用这个表(例如,客户/服务器应用都在访问这个表,而且还在通过Web访问),我则倾向于选择
ORA_ROWSCN方法。这是因为,在现有的遗留应用中,可能不希望出现一个新列,或者即使我们另外增加一步把这个额外的列隐藏起来,为了维护这个列,可能需要一个必要的触发器,而这个触发器的开销非常大,这是我们无法承受的。ORA_ROWSCN技术没有干扰性,而且在这个方面是轻量级的(当然,这是指我们执行表的重建之后)。
散列/校验和方法在数据库独立性方面很不错,特别是如果我们在数据库之外计算散列或校验和,则更是如此。不过,如果在中间层而不是在数据库中执行计算,从CPU使用和网络传输方面来看,就会带来更大的资源使用开销。

阻塞

如果一个会话持有某个资源的锁,而另一个会话在请求这个资源,就会出现阻塞(blocking),请求的会话会被阻塞,它会“挂起”,直至持有锁的会话放弃锁定的资源。几乎在所有情况下,阻塞都是可以避免的。
数据库中有5条常见的DML语句可能会阻塞,具体是:INSERT、UPDATE、DELETE、MERGE和SELECT FOR UPDATE。对于一个阻塞的SELECT FOR UPDATE,解决方案很简单:只需增加NOWAIT子句,它就不会阻塞了。这样一来, 你的应用会向最终用户报告,这一行已经锁定。
1. 阻塞的INSERT
你有一个带主键的表,或者表上有惟一的约束,但有两个会话试图用同样的值插入一行,其中一个会话就会阻塞, 直到另一个会话提交或者回滚为止:如果另一个会话提交,那么阻塞的会话会收到一个错误,指出存在一个重复值;倘若另一个会话回滚,在这种情况下,阻塞的会 话则会成功。还有一种情况,可能多个表通过引用完整性约束相互链接。对子表的插入可能会阻塞,因为它所依赖的父表正在创建或删除。
为避免这种情况,最容易的做法是使用一个序列来生成主键/惟一列值序列(sequence)设计为一种高度并发的方法,用在多用户环境中生成惟一键。如果无法使用序列,那可以使用手工锁来避免这个问题,这里的手工锁通过内置的DBMS_LOCK包来实现。
对于插入,不会选择现有的行,也不会对现有的行锁定。没有办法避免其他人插入值相同的行,如果别人真的插入了具有相同值的行,这会阻塞我们的会话,而导致我们无休止地等待。此时,DBMS_LOCK就能派上用场了。下面创建一个带主键的表,还有一个触发器,它会防止两个(或更多)会话同时插入相同的值。这个触发器使用DBMS_UTILITY.GET_ HASH_VALUE来计算主键的散列值,得到一个0~1 073 741 823之间的数(这也是Oracle允许我们使用的锁ID号的范围)。在这个例子中,选择了一个大小为1 024的散列表,这说明我们会把主键散列到1 024个不同的锁ID。然后使用DBMS_LOCK.REQUEST根据这个ID分配一个排他锁(也称独占锁,exclusive lock)。一次只有一个会话能做这个工作,所以,如果有人想用相同的主键值向表中插入一条记录,这个人的锁请求就会失败(并且会产生resource busy(资源忙)错误):                       
scott@ORCL>create table demo ( x int primary key );
表已创建。

scott@ORCL>create or replace trigger demo_bifer
  2  before insert on demo
  3  for each row
  4  declare
  5     l_lock_id number;
  6     resource_busy exception;
  7     pragma exception_init( resource_busy, -54 );
  8  begin
  9     l_lock_id :=
 10     dbms_utility.get_hash_value( to_char( :new.x ), 0, 1024 );
 11     if ( dbms_lock.request
 12             ( id => l_lock_id,
 13             lockmode => dbms_lock.x_mode,
 14             timeout => 0,
 15             release_on_commit => TRUE ) <> 0 )
 16     then
 17             raise resource_busy;
 18     end if;
 19  end;
 20  /

触发器已创建
现在,如果在两个单独的会话中执行下面的插入:
scott@ORCL>insert into demo values ( 1 );
已创建 1 行。
第一个会话会成功,但是紧接着第二个会话中会得出以下错误:                                                                                                                                                                                
scott@ORCL>insert into demo values ( 1 );
insert into demo values ( 1 )
            *
第 1 行出现错误:
ORA-00054: 资源正忙, 但指定以 NOWAIT 方式获取资源, 或者超时失效
ORA-06512: 在 "SCOTT.DEMO_BIFER", line 14
ORA-04088: 触发器 'SCOTT.DEMO_BIFER' 执行过程中出错
这里的思想是: 为表提供的主键值要受触发器的保护,并把它放入一个字符串中。然后可以 使用DBMS_UTILITY.GET_HASH_VALUE为这个串得出一个“几乎惟一”的散列值。只要使用小于1 073 741 823的散列表,就可以使用DBMS_LOCK独占地“锁住”这个值。
计算散列之后,取得这个散列值,并 使用DBMS_LOCK来请求将这个锁ID独占地锁住(超时时间为ZERO,这说明如果已经有人锁住了这个值,它会立即返回)。如果超时或者由于某种原因失败了,将产生ORA-54 Resource Busy(资源忙)错误。否则什么也不做,完全可以顺利地插入,不会阻塞。                                                                                 当然,如果表的主键是一个INTEGER,而你不希望这个主键超过1 000 000 000,那么可以跳过散列,直接使用这个数作为锁ID。要适当地设置散列表的大小(在这个例子中,散列表的大小是1 024),以避免因为不同的串散列为同一个数(这称为散列冲突)而人工地导致资源忙消息。散列表的大小与特定的应用(数据)有关,并发插入的数量也会影响散列表的大小。最后, 尽管Oracle有无限多个行级锁,但是enqueue锁(这是一种队列锁)的个数则是有限的如果在会话中插入大量行,而没有提交,可能就会发现创建了太多的enqueue队列锁,而耗尽了系统的队列资源(超出了ENQUEUE_RESOURCES系统参数设置的最大值),因为每行都会创建另一个enqueue锁。如果确实发生了这种情况,就需要增大ENQUEUE_RESOURCES参数的值。还可以向触发器增加一个标志,允许打开或关闭这种检查。例如,如果我准备插入数百条或数千条记录,可能就不希望启用这个检查。
2. 阻塞的Merge、Update和Delete                                                                                                                                                                                                                                 在一个交互式应用中,可以从数据库查询某个数据,允许最终用户处理这个数据,再把它“放回”到数据库中,此时如果UPDATE或DELETE阻塞,就说明你的代码中可能存在一个丢失更新问题。你试图UPDATE(更新)其他人正在更新的行(换句话说,有人已经锁住了这一行)。通过使用SELECT FOR UPDATE NOWAIT查询可以避免这个问题,这个查询能做到:
1.验证自从你查询数据之后数据未被修改(防止丢失更新)。
2.锁住行(防止UPDATE或DELETE被阻塞)。
如前所述,不论采用哪一种锁定方法都可以这样做。不论是悲观锁定还是乐观锁定都可以利用SELECT FOR UPDATE NOWAIT查询来验证行未被修改。悲观锁定会在用户有意修改数据那一刻使用这条语句。乐观锁定则在即将在数据库中更新数据时使用这条语句。这样不仅能解决应用中的阻塞问题,还可以修正数据完整性问题。
由于MERGE只是INSERT和UPDATE,所以可以同时使用这两种技术

死锁                                                                                                                                                                                                                   

如果有两个会话,每个会话都持有另一个会话想要的资源,此时就会出现死锁(deadlock)。例如,如果我的数据库中有两个表A和B,每个表中都只有一行。打开两个会话,在会话A中更新表A,在会话B中更新表B。然后在会话B中更新表A,就会阻塞。会话A已经锁定了这一行。这不是死锁,只是阻塞,因为会话A还有机会提交或回滚,这样会话B就能继续。
如果再回到会话A,更新表B,这就会导致一个死锁。会话B中对表A的更新可能回滚,得到以下错误:  
update a set id=10000 where id=1
       *
第 1 行出现错误:
ORA-00060: 等待资源时检测到死锁
想要更新表B的会话A还阻塞着, Oracle不会回滚整个事务,只会回滚与死锁有关的某条语句。会话B仍然锁定着表B中的行,而会话A还在耐心地等待这一行可用。收到死锁消息后,会话B必须决定将表B上未执行的工作提交还是回滚。一旦会话B执行提交或回滚,另一个阻塞的会话A就会继续。
Oracle认为死锁很少见,而且由于如此少见,所以每次出现死锁时它都会在服务器上创建一个跟踪文件:                                                                                                                
*** 2018-04-25 15:53:01.455
*** ACTION NAME:() 2005-04-25 15:53:01.455
*** MODULE NAME:(SQL*Plus) 2005-04-25 15:53:01.455
*** SERVICE NAME:(SYS$USERS) 2005-04-25 15:53:01.455
*** SESSION ID:(145.208) 2005-04-25 15:53:01.455
DEADLOCK DETECTED
Current SQL statement for this session:
update a set x = 1
The following deadlock is not an ORACLE error. It is a
deadlock due to user error in the design of an application
or from issuing incorrect ad-hoc SQL. The following
information may aid in determining the deadlock:...
显然, Oracle认为这些应用死锁是应用自己导致的错误。Oracle中极少出现死锁,甚至可以认为几乎不存在。通常情况下,必须人为地提供条件才会产生死锁。
导致死锁的头号原因是 外键未加索引(第二号原因是 表上的位图索引遭到并发更新)。在以下两种情况下,Oracle在修改父表后会对子表加一个全表锁:
1. 如果更新了父表的主键,由于外键上没有索引,所以子表会被锁住。
2.如果删除了父表中的一行,整个子表也会被锁住(由于外键上没有索引)。
在Oracle9i及以上版本中, 这些全表锁都是短期的,这意味着它们仅在DML操作期间存在,而不是在整个事务期间都存在。即便如此,这些全表锁还是可能(而且确实会)导致很严重的锁定问题。
下面说明第二点,如果用以下命令建立了两个表:
scott@ORCL>create table p ( x int primary key );
表已创建。

scott@ORCL>create table c ( x references p );
表已创建。

scott@ORCL>insert into p values ( 1 );
已创建 1 行。

scott@ORCL>insert into p values ( 2 );
已创建 1 行。

scott@ORCL>commit;
提交完成。
然后执行以下语句:
scott@ORCL>insert into c values ( 2 );
已创建 1 行。
到目前为止,还没有什么问题。但是如果再到另一个会话中,试图删除第一条父记录:
scott@ORCL>delete from p where x = 1;
此时就会发现,这个会话立即被阻塞了。它在执行删除之前试图对表C加一个全表锁。现在,别的会话都不能对C中的任何行执行DELETE、INSERT或UPDATE(已经开始的会话可以继续,但是新会话将无法修改C)。
更新主键值也会发生这种阻塞。假设我们使用了Oracle Forms,并为表创建了一个默认布局。默认情况下,Oracle Forms会生成一个更新,对我们选择要显示的表中的每一列进行修改。如果在DEPT表中建立一个默认布局,包括3个字段,只要我们修改了DEPT表中的任何列,Oracle Forms都会执行以下命令:                                                  
update dept set deptno=:1,dname=:2,loc=:3 where rowid=:4
在这种情况下, 如果EMP表有DEPT的一个外键,而且在EMP表的DEPTNO列上没有任何索引,那么更新DEPT时整个EMP表都会被锁定。即便主键值没有改变,执行前面的SQL语句后,子表EMP也会被锁定。如果使用Oracle Forms,解决方案是把这个表的UPDATE CHANGED COLUMNS ONLY属性设置为YES。这样一来,Oracle Forms会生成一条UPDATE语句,其中只包含修改过的列(而不包括主键)。
删除父表中的一行可能导致子表被锁住如果删除表P中的一行,那么在DML操作期间,子表C就会锁定,这样能避免事务期间对C执行其他更新(前提:没有人在修改C,如果确实已经有人在修改C,删除会等待)。此时就会出现阻塞和死锁问题。通过锁定整个表C,数据库的并发性就会大幅下降,以至于没有人能够修改C中的任何内容。另外,出现死锁的可能性则增加了,因为我的会话现在“拥有”大量数据,直到提交时才会交出。其他会话因为C而阻塞的可能性也更大;只要会话试图修改C就 会被阻塞。因此,数据库中大量会话被阻塞,这些会话持有另外一些资源的锁。实际上, 如果其中任何阻塞的会话锁住了我的会话需要的资源,就会 出现一个死锁。在这种情况下,造成死锁的原因是:我的会话不允许别人访问超出其所需的更多资源(在这里就是一个表中的所有行)。如果数据库中存 在死锁, 查看是不是存在未加索引的外键。只需对外键加索引,死锁(以及大量其他的竞争问题)都会烟消云散。下面的例子展示了如何找出表C中未加索引的外键:                                                                                                                            
scott@ORCL>column columns format a30 word_wrapped
scott@ORCL>column tablename format a15 word_wrapped
scott@ORCL>column constraint_name format a15 word_wrapped
scott@ORCL>select table_name, constraint_name,
  2  cname1 || nvl2(cname2,','||cname2,null) ||
  3  nvl2(cname3,','||cname3,null) || nvl2(cname4,','||cname4,null) ||
  4  nvl2(cname5,','||cname5,null) || nvl2(cname6,','||cname6,null) ||
  5  nvl2(cname7,','||cname7,null) || nvl2(cname8,','||cname8,null)
  6  columns
  7  from ( select b.table_name,
  8                     b.constraint_name,
  9                     max(decode( position, 1, column_name, null )) cname1,
 10                     max(decode( position, 2, column_name, null )) cname2,
 11                     max(decode( position, 3, column_name, null )) cname3,
 12                     max(decode( position, 4, column_name, null )) cname4,
 13                     max(decode( position, 5, column_name, null )) cname5,
 14                     max(decode( position, 6, column_name, null )) cname6,
 15                     max(decode( position, 7, column_name, null )) cname7,
 16                     max(decode( position, 8, column_name, null )) cname8,
 17                     count(*) col_cnt
 18     from (select substr(table_name,1,30) table_name,
 19                             substr(constraint_name,1,30) constraint_name,
 20                             substr(column_name,1,30) column_name,
 21                             position
 22                             from user_cons_columns ) a,
 23             user_constraints b
 24     where a.constraint_name = b.constraint_name
 25     and b.constraint_type = 'R'
 26     group by b.table_name, b.constraint_name
 27     ) cons
 28  where col_cnt > ALL
 29  ( select count(*)
 30     from user_ind_columns i
 31     where i.table_name = cons.table_name
 32     and i.column_name in (cname1, cname2, cname3, cname4,
 33     cname5, cname6, cname7, cname8 )
 34     and i.column_position <= cons.col_cnt
 35     group by i.index_name
 36  )
 37  /

TABLE_NAME                     CONSTRAINT_NAME COLUMNS
------------------------------ --------------- ------------------------------
C                              SYS_C0012372    X
EMP                            FK_DEPTNO       DEPTNO
这个脚本将处理外键约束,其中最多可以有8列。首先,它在前面的查询中建立一个名为CONS的内联视图(inline view)。这个内联视图将约束中适当的列名从行转置到列,其结果是每个约束有一行,最多有8列,这些列分别取值为约束中的列名。另外,这个视图中还有一个列COL_CNT,其中包含外键约束本身的列数。对于这个内联视图中返回的每一行,我们要执行一个关联子查询(correlated subquery),检查当前所处理表上的所有索引。它会统计出索引中与外键约束中的列相匹配的列数,然后按索引名分组。这样,就能生成一组数,每个数都是该表某个索引中匹配列的总计。如果原来的COL_CNT大于所有这些数,那么表中就没有支持这个约束的索引。如果COL_CNT小于所有这些数,就至少有一个索引支持这个约束。注意,这里使用了NVL2函数,我们用这个函数把列名列表“粘到”一个用逗号分隔的列表中。这个函数有3个参数:A、B和C。如果参数A非空,则返回B;否则返回参数C。这个查询有一个前提,假设约束的所有者也是表和索引的所有者。如果另一位用户对表加索引,或者表在另一个模式中(这两种情况都很少见),就不能正确地工作。
表C在列X上有一个外键,但是没有索引。通过对X加索引,就可以完全消除这个锁定问题。除了全表锁外,在以下情况下,未加索引的外键也可能带来问题:
1.如果有
ON DELETE CASCADE,而且没有对子表加索引:例如,EMP是DEPT的子表,DELETE DEPTNO = 10应该CASCADE(级联)至EMP。如果EMP中的DEPTNO没有索引,那么删除DEPT表中的每一行时都会对EMP做一个全表扫描。如果从父表删除多行,父表中每删除一行就要扫描一次子表。
2.从父表查询子表:再次考虑EMP/DEPT例子。如果频繁地运行以下查询,你会发现没有索引会使查询速度变慢:
2.1 select * from dept, emp
2.2 where emp.deptno = dept.deptno and dept.deptno = :X;
当满足下述全部3个条件,不需要对外键加索引
1. 没有从父表删除行
2. 没有更新父表的惟一键/主键值
3. 没有从父表联结子表(如DEPT联结到EMP)
 

锁升级 lock escalation

出现锁升级时,系统会降低锁的粒度。举例来说,数据库系统可以把一个表的100个行级锁变成一个表级锁。现在你用的是“能锁住全部的一个锁”,一般而言,这还会锁住以前没有锁定的大量数据。如果数据库认为锁是一种稀有资源,而且想避免锁的开销,这些数据库中就会频繁使用锁升级。
Oracle不会升级锁,从来不会,但是它会执行锁转换(lock conversion)或锁提升(lock promotion)。

Oracle会尽可能地在最低级别锁定(也就是说,限制最少的锁),如果必要,会把这个锁转换为一个更受限的级别。例如,如果用FOR UPDATE子句从表中选择一行,就会创建两个锁。一个锁放在所选的行上(这是一个排他锁;任何人都不能以独占模式锁定这一行)。另一个锁是ROW SHARE TABLE锁,放在表本身上。这个锁能防止其他会话在表上放置一个排他锁,举例来说,这样能相应地防止这些会话改变表的结构。另一个会话可以修改这个表中的任何其他行,而不会有冲突。假设表中有一个锁定的行,这样就可以成功执行尽可能多的命令。
锁升级不是一个数据库“特性”。如果数据库支持锁升级,就说明这个数据库的锁定机制中存在某些内部开销,而且管理数百个锁需要做大量的工作。在Oracle中,1个锁的开销与1 000 000个锁是一样的,都没有开销。

猜你喜欢

转载自blog.csdn.net/a0001aa/article/details/79957354
今日推荐