开发多用户、数据库驱动的应用时,最大的难点之一是:一方面要力争取得最大限度 的并发访问,与此同时还要确保每个用户能以一致的方式读取和修改数据。为此就有了锁定(locking)机制,这也是所有数据库都具有的一个关键特性。我 们来看看Oracle 的锁定机制和别的数据库有什么不同。
所谓的锁机制是用于共享资源的并发访问。在单用户数据库中,并不需要锁。如果有多个用户访问或修改数据或和数据结构,就需要一种机制来防止对同一信息的并 发修改,这就是锁的用处。
Oracle数据库的锁机制和别的都不同(可以说每种数据库本质上都有不同),在Oracle中,有几点要注意:
1).事务是每个数据库的核心,它们是“好东西”。
2).应该延迟到适当的时刻才提交。不要太快提交,以避免对系统带来压力。这是因为,如果事务很长或很大,一般不会对系统有压力。相应的原则是:在必要时
才提交,但是此前不要提交。事务的大小只应该根据业务逻辑来定。
3).只要需要,就应该尽可能长时间地保持对数据所加的锁。这些锁是你能利用的工具,而不是让你退避三舍的东西。锁不是稀有资源。恰恰相反,只要需要,你
就应该长期地保持数据上的锁。锁可能并不稀少,而且它们可以防止其他会话修改信息。
4).在Oracle中,行级锁没有相关的开销,根本没有。不论你是有1个行锁,还是1 000
000个行锁,专用于锁定这个信息的“资源”数都是一样的。当然,与修改1行相比,修改1 000 000行要做的工作肯定多得多,但是对1 000
000行锁定所需的资源数与对1行锁定所需的资源数完全相同,这是一个固定的常量。
5).不要以为锁升级“对系统更好”(例如,使用表锁而不是行锁)。在Oracle中,锁升级(lock
escalate)对系统没有任何好处,不会节省任何资源。也许有时会使用表锁,如批处理中,此时你很清楚会更新整个表,而且不希望其他会话锁定表中的
行。但是使用表锁绝对不是为了避免分配行锁,想以此来方便系统。
6).可以同时得到并发性和一致性。每次你都能快速而准确地得到数据。数据读取器不会被数据写入器阻塞。数据写入器也不会被数据读取器阻塞。这是
Oracle与大多数其他关系数据库之间的根本区别之一。
锁定问题有:丢失更新。处理的办法有两种:
1.悲观锁定
这种锁定机制就是用户一开始修改某行时,就立刻放上一个行锁。悲观锁定(pessimistic locking)仅用于有状态(stateful)或有连接(connected)环境,也就是说,你的应用与数据库有一条连续的连接,而且至少在事务生 存期中只有你一个人使用这条连接。这是20世纪90年代中期客户/服务器应用中的一种流行做法。每个应用都得到数据库的一条直接连接,这条连接只能由该应 用实例使用。这种采用有状态方式的连接方法已经不太常见了(不过并没有完全消失),特别是随着20世纪90年代中后期应用服务器的出现,有状态连接更是少 见。
这种锁定很悲观,因为我们不能对该行能否未改变持怀疑态度。
2.乐观锁定
这种锁定机制是把所有锁定都延迟到即将执行更新才做。在修改信息时不会上锁,我们很乐观,认为数据不会被其他用户修改;因此,会等到最后一刻才去看我们的 想法对不对。当他在更新的时候,发现数据被修改了,就得重新再来。
实现乐观锁定有很多方法,一下简单介绍三种:
a) 使用版本列的乐观锁定
这是一个简单的实现,如果你想保护数据库表不出现丢失更新问题,应对每个要保护的表增加一列。这一列一般是NUMBER或DATE/TIMESTAMP 列,通常通过表上的一个行触发器来维护。每次修改行时,这个触发器要负责递增NUMBER列中的值,或者更新DATE/TIMESTAMP列。但是触发器 会引入大量开销,建议还是不使用这种办法。
b) 使用校验和的乐观锁定
这与前面的版本列方法很相似,不过在此要使用基数据本身来计算一个“虚拟的”版本列。与 使用版本列的做法一样,我们可以采用同样的方法使用这些散列值或校验和,只需把从数据库读出数据时得到的散列或校验和值与修改数据前得到的散列或校验和值 进行比较。在我们读出数据之后,但是在修改数据之前,如果有人在这段时间内修改了这一行的值,散列值或校验和值往往会大不相同。
c) 使用 ORA_ROWSCN的乐观锁定
从Oracle 10g Release 1开始,你还可以使用内置的ORA_ROWSCN函数。它的工作与前面所述的版本列技术很相似,但是可以由Oracle自动执行,而不需要在表中增加额外 的列,也不需要额外的更新/维护代码来更新这个值。
ORA_ROWSCN建立在内部Oracle系统时钟(SCN)基础上。在Oracle中,每次提交时,SCN都会推进(其他情况也可能导致SCN推进, 要注意,SCN只会推进,绝对不会后退)
但是有个地方要注意,Oracle在默认情况下会采用块级推进ORA_ROWSCN,也就是说在一个块内多行会共享相同的ORA_ROWSCN,如果更新 一个块上的某一行,而且这个块上还有另外50行,那么这些行的ORA_ROWSCN也会推进。这往往会导致许多假警报,你认为某一行已经修改,但实际上它 并没有改动。因此,需要注意这一点,并了解如何改变这种行为。
要改变这种默认的行为,我们必须启用ROWDEPENDENCIES再重新创建这个段。可以使用DBMS_REDEFINITION中(Oracle提供 的另一个包)的在线重建功能来执行,但是对于小表可以直接删掉再重建。一个例子是:
ops$tkyte@ORA10G> create table
dept
(deptno, dname, loc, data,
constraint dept_pk primary key(deptno)
)
ROWDEPENDENCIES
as
select deptno, dname, loc, rpad('*',3500,'*')
from scott.dept;
(二)阻塞
如果一个会话持有某个资源的锁,而另一个会话在请求这个资源,就会出现阻塞(blocking)。这样一来,请求的会话会被阻塞,它会“挂起”,直至持有 锁的会话放弃锁定的资源。几乎在所有情况下,阻塞都是可以避免的。
数据库中有5条常见的DML语句可能会阻塞,具体是:INSERT、UPDATE、DELETE、MERGE和SELECT FOR UPDATE。对于一个阻塞的SELECT FOR UPDATE,解决方案很简单:只需增加NOWAIT子句,它就不会阻塞了。
a) Insert 阻塞
INSERT阻 塞的情况不多见。最常见的情况是,你有一个带主键的表,或者表上有惟一的约束,但有两个会话试图用同样的值插入一行。如果是这样,其中一个会话就会阻塞, 直到另一个会话提交或者回滚为止:如果另一个会话提交,那么阻塞的会话会收到一个错误,指出存在一个重复值;倘若另一个会话回滚,在这种情况下,阻塞的会 话则会成功。还有一种情况,可能多个表通过引用完整性约束相互链接。对子表的插入可能会阻塞,因为它所依赖的父表正在创建或删除。
b) 阻塞的Merge、Update和Delete
在一个交互式应用中,可以从数据库查询某个数据,允许最终用户处理这个数据,再把它“放回”到数据库中,此时如果UPDATE或DELETE阻塞,就说明 你的代码中可能存在一个丢失更新问题(如果真是这样,按我的说法,就是你的代码中存在bug)。你试图UPDATE(更新)其他人正在更新的行(换句话 说,有人已经锁住了这一行)。通过使用SELECT FOR UPDATE NOWAIT查询可以避免这个问题
(三) 死锁
如果你有两个会话,每个会话都持有另一个会话想要的资源,此时就会出现死锁(deadlock)。例如,如果我的数据库中有两个表A和B,每个表中都只有 一行,就可以很容易地展示什么是死锁。我要做的只是打开两个会话(例如,两个SQL*Plus会话)。在会话A中更新表A,并在会话B中更新表B。现在, 如果我想在会话B中更新表A,就会阻塞。会话A已经锁定了这一行。这不是死锁;只是阻塞而已。我还没有遇到过死锁,因为会话A还有机会提交或回滚,这样会 话B就能继续。如果我再回到会话A,试图更新表B,这就会导致一个死锁。要在这两个会话中选择一个作为“牺牲品”,让它的语句回滚。
根据我的经验,导致死锁的头号原因是外键未加索引(这是因为加了索引后,删除父表记录后就不需要对子表加表级锁)例如:
a)
如果更新了父表的主键(倘若遵循关系数据库的原则,即主键应当是不可变的,这种情况就很少见),由于外键上没有索引,所以子表会被锁住。
b) 如果删除了父表中的一行,整个子表也会被锁住(由于外键上没有索引)。
第二种情况比较常见,因为在删除父表的一行时,那么在DML操作期间,子表会被锁定,这样能避免事务期间对子表执行其他更新,但确实有人在修改子表时,删 除则会等待。只有在外键加了索引后,才不会对子表加表级锁(这是为什么呢?)
(四) 锁类型
Oracle中主要有3类锁,具体是:
a).DML锁(DML lock):DML代表数据操纵语言(Data Manipulation Language)。一般来讲,这表示SELECT、INSERT、UPDATE、MERGE和DELETE语句。DML锁机制允许并发执行数据修改。例 如,DML锁可能是特定数据行上的锁,或者是锁定表中所有行的表级锁。
b).DDL锁(DDL lock):DDL代表数据定义语言(Data Definition Language),如CREATE和ALTER语句等。DDL锁可以保护对象结构
c).内部锁和闩:Oracle使用这些锁来保护其内部数据结构。例 如,Oracle解析一个查询并生成优化的查询计划时,它会把库缓存“临时闩”,将计划放在那里,以供其他会话使用。闩(latch)是Oracle采用 的一种轻量级的低级串行化设备,功能上类似于锁。不要被“轻量级”这个词搞糊涂或蒙骗了,你会看到,闩是数据库中导致竞争的一个常见原因。轻量级指的是闩 的实现,而不是闩的作用
DML锁的有:
1) TX锁(事务锁)
事务发起第一个修改时会得到TX锁(事务锁),而且会一直持有这个锁,直至事务执行提交(COMMIT)或回滚(ROLLBACK)。TX锁用作一种排队
机制,使得其他会话可以等待这个事务执行。Oracle并
没有一个传统的锁管理器,不会用锁管理器为系统中锁定的每一行维护一个长长的列表。不过,其他的许多数据库却是这样做的,因为对于这些数据库来说,锁是一
种稀有资源,需要对锁的使用进行监视。使用的锁越多,系统要管理的方面就越多,所以在这些系统中,如果使用了“太多的”锁就会有问题。
如果数据库中有一个传统的基于内存的锁管理器,在这样一个数据库中,对一行锁定的过程一般如下:
(1) 找到想锁定的那一行的地址。
(2) 在锁管理器中排队(锁管理器必须是串行化的,因为这是一个常见的内存中的结构)。
(3) 锁定列表。
(4) 搜索列表,查看别人是否已经锁定了这一行。
(5) 在列表中创建一个新的条目,表明你已经锁定了这一行。
(6) 对列表解锁。
既然已经锁定了这一行,接下来就可以修改它了。之后,在你提交修改时,必须继续这个过程,如下:
(7) 再次排队。
(8) 锁住锁的列表。
(9) 在这个列表中搜索,并释放所有的锁。
(10) 对列表解锁。
Oracle不是这样做的。Oracle中的锁定过程如下:
(1) 找到想锁定的那一行的地址。
(2) 到达那一行。
(3) 锁定这一行(如果这一行已经锁定,则等待锁住它的事务结束,除非使用了NOWAIT选项)
2)TM (DML Enqueue)锁
TM锁(TM lock)用于确保在修改表的内容时,表的结构不会改变。例如,如果你已经更新了一个表,会得到这个表的一个TM锁。这会防止另一个用户在该表上执行DROP或ALTER命令。
尽管每个事务只能得到一个TX锁,但是TM锁则不同,修改了多少个对象,就能得到多少个TM锁。在此,有意思的是,TM锁的ID1列就是DML锁定对象的对象ID,所以,很容易发现哪个对象持有这个锁。
这个锁主要用于保护数据,和DDL锁有不同的地方。
DDL锁有:
在DDL操作中会自动为对象加DDL锁(DDL Lock),从而保护这些对象不会被其他会话所修改。例如,如果我执行一个DDL操作ALTERTABLE T,表T上就会加一个排他DDL锁,以防止其他会话得到这个表的DDL锁和TM锁。在DDL语句执行期间会一直持有DDL锁,一旦操作执行就立即释放DDL锁。实际上,通常会把DDL语句包装在隐式提交(或提交/回滚对)中来执行这些工作。由于这个原因,在Oracle中DDL一定会提交。
因此,DDL总会提交(即使提交不成功也会如此)。DDL一开始就提交,一定要知道这一点。它首先提交,因此如果必须回滚,它不会回滚你的事务。如果你执行了DDL,它会使你所执行的所有未执行的工作成为永久性的,即使DDL不成功也会如此。如果你需要执行DDL,但是不想让它提交你现有的事务,就可以使用一个自治事务(autonomous transaction)。
有3种类型的DDL锁:
1)排他DDL锁(Exclusive DDL lock):这会防止其他会话得到它们自己的DDL锁或TM(DML)锁。这说明,在DDL操作期间你可以查询一个表,但是无法以任何方式修改这个表。
2)共享DDL锁(Share DDL lock):这些锁会保护所引用对象的结构,使之不会被其他会话修改,但是允许修改数据。
3) 可中断解析锁(Breakable parse locks):这些锁允许一个对象(如共享池中缓存的一个查询计划)向另外某个对象注册其依赖性。如果在被依赖的对象上执行DDL,Oracle会查看已经对该对象注册了依赖性的对象列表,并使这些对象无效。因此,这些锁是“可中断的”,它们不能防止DDL出现。
最后看看闩,闩(latch)是轻量级的串行化设备,用于协调对共享数据结构、对象和文件的多用户访问。
闩就是一种锁,设计为只保持极短的一段时间(例如,修改一个内存中数据结构所需的时间)。闩用于保护某些内存结构,如数据库块缓冲区缓存或共享池中的库缓存。
这表明闩是专门用于保护Oracle内部的数据结构的,锁是用于保护数据表等用户的共享资源。
还有闩的自旋的一些概念这里就不提了。