锁(lock)和闩(latch)
开发多用户、数据库驱动的应用时,最大的难点之一是:一方面要力争取得最大限度的并发访问,与此同时还要确保每个用户能以一致的方式读取和修改数据。为此就有了锁定(locking)机制,这也是所有数据库都具有的一个关键特性,oracle在这方面更是技高一筹。
5.1 什么是锁
锁(lock)机制用于管理对共享资源的并发访问。注意,这里说的是“共享资源”而不是“数据库行”。Oracle会在行级对表数据锁定,不过oracle也会在其他多个级别上使用锁,从而对多种不同的资源提供并发访问。例如,执行一个存储过程时,过程本身会以某种模式锁定,以允许其他用户执行这个过程,但是不允许另外的用户以任何方式修改这个过程。数据库中使用锁是为了支持对共享资源进行并发访问,与此同时还能提供数据完整性和一致性。
需要了解的是有多少种数据库,其中就可能有多少种实现锁定的方法。你可能对某个特定的关系型数据库管理系统(RDBMS)的锁定模型有一定的经验,但是每个数据库中对实现锁定的方式都大相径庭。
在oracle中,你会了解到下面几点:
1.事务是每个数据库的核心,他们是好东西。
2.应该延迟到适当的时刻才提交。不要太快提交,以避免对系统带来压力。这是因为,如果事务很长或很大,一般不会对系统有压力。相应的原则是:在必要时才提交,事务的大小只应该根据业务逻辑来定。
3.只要需要,就应该尽可能长时间地保持对数据所加的锁。这些锁是你能利用的工具,而不是然给你退避三舍的东西。锁不是稀有资源(而在比如sqlserver中,这样做是疯狂的,会极大的降低性能)。
4.在oracle中,行级锁没有相关的开销,根本没有。不论你是有1个行级锁,还是1000000个行级锁,专用于锁定这个信息的资源数是一样的。当然,与修改1行数据相比,修改1000000行要做的工作肯定多得多,但是对1000000行锁定所需的资源数与对1行锁定所需的资源完全相同,这是一个固定的常量。
5.不要以为锁升级“对系统更好”(例如,使用表锁而不是行锁)。在oracle中,锁升级对系统没有任何好处,不会节省任何资源。
6.可以同时得到并发性和一致性。每次你都能快速而准确地得到数据。数据读取器不会被数据写入器阻塞。数据写入器也不会被数据读取器阻塞。这是oracle与大多数其他关系数据库之间的根本区别之一。
5.2 锁定问题
讨论oracle使用的各种类型的锁之前,先了解一些锁定问题会有好处,其中很多问题都是因为应用设计不当,没有正确使用数据库锁定机制产生的。
5.2.1 丢失更新
丢失更新(lost update)是一个经典的数据库问题。简单地说,出现下面的情况时就会发生丢失更新。
(1)会话session1中的一个事务获取(查询)一行数据,放入本地内存,并显示给一个最终用户user1.
(2)会话session2中的另一个事务也获取这一行,但是将数据显示给最终用户user2.
(3)User1使用应用修改这一样,然该应用更新数据库并提交。会话session1的事务现在已经执行。
(4)User2也修改这一样,让应用更新数据库并提交。会话session2的事务现在已经执行。
许多工具可以保护你避免这种情况,如oracle forms和apex,这些工具能确保从查询记录的那个时刻起,这个记录没有改变,而且对他执行任何修改时都会将其锁定,但是其他程序做不到这点(如手写的VB或java)。实际上就是要使用某种锁定策略,共有两种锁定策略:悲观锁定或乐观锁定。
5.2.2 悲观锁定
用户在屏幕上修改值之前,这个锁定方法就要起作用。例如,用户一旦有意对他选择的某个特定行执行更新,就会放上一个行级锁。这个行锁会持续到程序对行的修改提交。
悲观锁定(pessimistic locking)仅用于有状态(stateful)或有连接(connected)环境,也就是说,你的应用与数据库由一条连续的连接,而且至少在事务生存期中只有你一个人使用这条连接。这是20世纪90年代的一种流行做法。每个应用格斗得到数据库的一条直接连接,这条连接只能由该实例使用,这种采用有状态的连接方法已经不太常见了。
我们可以使用select * … for update来显式的获取锁定。
5.2.3 乐观锁定
第二种方法称为乐观锁定(optimistic locking),即把所有锁定都延迟到即将执行更新之前才做。这种锁定方法在所有环境中都行得通,但是采用这种方法的话,执行更新的用户“失败”的可能性会加大。这说明,这个用户要更新它的数据行时,发现数据已经修改过,所以他必须从头再来。可以使用select for update nowait来验证行是否未被修改,并在即将update之前锁定来避免被另一个会话阻塞。
这里说下select for udate和select for update nowait/wait(n)
前者会主动获取锁,如果已经被别人获得,则一直阻塞等待。后者则无论获得锁还是不获得都会立刻或等待n秒后返回。获得锁后都是执行了显式或隐式的commit或rollback后释放锁。
实现乐观并发控制的方法有很多种。比如:
1.使用一个特殊的列,这个列由一个数据库触发器或应用程序代码维护,可以告诉我们记录的最新“版本号”。
如果应用要实现乐观并发控制,只需要保存这个附加列的值。应用验证请求更新那一刻数据库中这一列的值与最初读取的值是否匹配,如果两个值相等,则说明这一行未被修改过。
2.使用一个校验或散列值,这是使用原来的数据计算得出的。
这个与前面的版本列方法很类似,不过在此要使用基数据本身来计算一个虚拟的版本列。
5.2.4 乐观锁定还是悲观锁定
那么哪种方法更好呢?根据Tom的经验,悲观锁定在oracle中工作地非常好,而且与乐观锁定相比,悲观锁定有很多优点。不过,它需要与数据库有一条有状态的连接,如客户/服务器连接,因为无法跨连接持有锁。正是因为这一点,在当前的许多情况下,悲观锁定不太现实。过去,客户/服务器应用可能只有数十个或数百个用户,对于这些应用,悲观锁定是不二选择。不过,如今对大多数应用,tom都建议采用乐观并发控制。
5.2.5 阻塞
如果一个会话持有某个资源的锁,而另一个会话在请求这个资源,就会出现阻塞(blocking)。这样一来,请求的会话会被阻塞,他会挂起,直到持有锁的会话放弃锁定的资源。几乎在所有情况下,阻塞都是可以避免的。
数据库有5条常见的DML语句可能会阻塞,具体是:insert,update,delete,merge和select for update。对于一个阻塞的select for update,很简单,只需要增加nowait子句就不阻塞了。我们看下另外4条DML语句,看看他们为什么不应阻塞,如果真的阻塞了应该如何修正。
1. 阻塞的insert
Insert阻塞的情况不多见。最常见的情况时,有一个带主键的表或表上有唯一约束。但有两个会话试图用同样的值插入一行。如果是这样,其中一个会话就会阻塞,直到另一个会话提交或回滚为止;如果另一个会话提交,那么阻塞的会话会受到一个错误,指出存在一个重复值;倘若另一个会话回滚,阻塞的会话则会成功。
如果应用允许最终用户生成主键/唯一列值,往往就会发生insert阻塞。为避免这种情况,最容易的做法是使用一个序列或SYS_GUID()生成主键/唯一列值。序列(sequence)内建函数设计为一种高度并发的方法,用在多用户环境中生成唯一键。
2. 阻塞的merge、update和delete
在一个交互式应用中,可以从数据库查询某个数据,允许最终用户处理这个数据,再把它“放回”到数据库中,此时如果update或delete阻塞,就说明你的代码中可能存在一个丢失更新问题,如果真这样,那也说明你的代码中存在bug。你试图update其他人正在更新的行。通过使用select for update nowait查询可以避免这个问题,这个查询能做到:
1.验证自从你查询数据之后数据未被修改(防止丢失更新)
2.锁住行(防止update 或delete被阻塞)。
5.2.6 死锁
如果有两个会话,每个会话都持有另一个会话想要的资源,此时就会出现死锁(deadlock)。根据tom的经验,导致死锁的头号原因是外键未加索引(第二号原因是表上的位图索引遭到并发更新)。在以下3种情况下,oracle在修改父表后会对子表加一个全表锁。
1. 如果更新了父表的主键,由于外键上没有索引,所以子表会被锁住。
2. 如果删除了父表中的一行,整个子表也会被锁住(由于外键上没有索引)。
3. 如果合并到父表,整个子表也会锁住(没有外键上的索引)。注意这一点只是用oracle 9i和10g,在11g开始不再成立。
但是,只要对外键加索引,这一切问题都解决了。
什么时候不需要对外键加索引呢?一般是当完全满足以下3个条件时:
1.没有从父表删除行
2.没有更新父表的唯一键/主键值
3.没有从父表联接子表
5.2.7 锁升级
出现锁升级(lock escalation)时,系统会降低锁的粒度。例如,数据库系统可以把一个表的100个行级锁变成一个表级锁。如果数据库认为锁是一种稀有资源,而且想避开锁的开销,这些数据库会频繁使用锁升级。注意:oracle从来不会升级锁。
5.3 锁类型
Oracle主要有3种锁。
1. DML锁(DML lock):DML代表数据操纵语言(Data Manipulation Language)。一般来讲,这表示select,insert,update,merge和delete语句。DML锁机制允许并发执行数据修改。
2. DDL锁(DDL lock):DDL代表数据定义语言(Data Definition Language),如create和alter语句等。DDL锁可以保护对象结构定义。
3. 内部锁和闩:oracle使用这些锁来保护其内部数据结构。例如,oracle解析一个查询并生成优化的查询计划时,他会把库缓存“临时闩”,将计划放在那里,以供其他会话使用。闩(latch)是oracle采用的一种轻量级的低级串行化设备,功能上类似于锁。不要被“轻量级”这个词搞糊涂或蒙骗了,你会看到,闩是数据库中导致竞争的一个常见原因。轻量级指的是latch的实现,而不是latch的作用。
5.3.1 DML锁
DML锁用于确保一次只有一个人能修改某一行,而且你正在处理一个表时别人不能删除这个表。在你工作时,oracle会透明程度不一地为你加这些锁。
1.TX锁
事务发起第一个修改时会得到TX锁(事务锁),而且会一直持有这个锁,直至事务执行提交或回滚。TX锁用作一种排队机制,使得其他会话可以等待这个事务执行。
在oracle中,闩为数据的一个属性。Oracle并没有一个传统的锁管理器,不会用锁管理器为系统中锁定的每一行维护一个长长的列表。不过,其他的许多数据库却是这样做的,对这些数据库来说,锁是一种稀有资源。
在oracle中的锁定过程如下:
(1) 找到想锁定的那一行的地址。
(2) 到达那一行。
(3) 锁定这一行
2. TM(DML Enqueue)锁
TM锁用于确保在修改表的内容时,表的结构不会改变。例如,如果已经更新了一个表,会得到这个表的一个TM锁。这会防止另一个用户在该表上执行DROP或ALTER命令。如果有表的一个TM锁,而另一位用户试图在这个表上执行DDL,就会得到报错ORA-00054:resource busy and acquire with NOWAIT specified
尽管每个事务只能得到一个TX锁,但是TM锁则不同,修改了多少个对象就能得到多少个TM锁。
5.3.2 DDL锁
在DDL操作中会自动为对象加上DDL锁,从而保护这些对象不会被其他会话所修改。例如,如果执行一个DDL操作alter table T,表T上就会加一个DDL锁,以防止其他会话得到这个表的DDL锁和TM锁。
在DDL语句执行期间会一直持有DDL锁,一旦操作执行就立即释放DDL锁。实际上,通常会把DDL语句包装在隐式提交(或提交/回滚对)中来执行。由于这个原因,在oracle中DDL一定会提交。每条create,alter等语句实际上都如下执行(伪代码):
begin commit; DDL-statement; commit; exception when others then rollback; end;
因此,DDL总会提交(即使提交不成功也会如此)。DDL一开始就提交。
有以下3种类型的DDL锁:
1.排他DDL锁(Exclusive DDL lock):这会防止其他会话得到他们自己的DDL锁或TM锁。这说明,在DDL操作期间可以查询一个表,但是无法以任何方式修改这个表。
2.共享DDL锁(Share DDL lock):这些所会保护所引用的结构,使之不会被其他会话修改,但是允许修改数据。
3.可中断解析锁(Breakable parse locks):这些锁允许一个对象向另外某个对象注册其依赖性。如果在被依赖的对象上执行DDL,oracle会查看已经对该对象注册了依赖性的对象列表,并使这些对象无效。因此,这些锁是可中断的,他们不能防止DDL出现。
5.3.3 闩(latch)
Latch是轻量级的串行化设备,用于协调对共享数据结构、对象和文件的多用户访问。
Latch就是一种锁,设计为只保持极短的一段时间(例如,修改一个内存中数据结构所需的时间)。Latch用于保护某些内存结构,如数据库快缓冲区或共享池中的库缓存。一般会在内部以一种“愿意等待”模式请求latch。这说明,如果latch不可用,请求会话会睡眠很短的一段时间,并在以后再次尝试这个操作。还可以采用一种“立即”(immediate)模式请求其他latch,这与select for update nowait的思想很相似。
Oracle使用诸如“测试和设置”(test and set)以及“比较和交换”(compare and swap)之类的原子命令来处理latch。
1. 闩“自旋”
关于latch还要了解一点,latch是一种锁,锁是串行化设备,而串行化设备会妨碍可扩展性。如果你的目标是构建一个能在oracle环境中很好地扩展的应用,就必须寻找合适的方法和解决方案,尽量减少所需执行的闩定的量。
等待latch可能是一个代价很高的操作。如果latch不是立即可用的,我们就得等待,在一台多cpu机器上,我们的会话就会自旋(spin),也就是说,在循环中反复地尝试来得到latch。出现自旋的原因是,上下文切换的开销很大,我们就会一直呆在cpu上,并立即再次尝试,而不是先睡眠放弃cpu。
同时,经过对比是否使用绑定变量,发现使用绑定变量的查询比不适用绑定变量耗费的硬件资源(cpu)少一个数量级,比如在解析阶段和等待latch阶段都节省很多资源。
补充自旋锁:
自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,即在标志寄存器中关闭/打开中断标志位,不需要自旋锁)。
何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。