6.1 什么是锁?
锁(lock)机制用于管理对共享资源的并发访问。
首先,所有数据库本质上都不同。其次,学习每一个新数据库时,应该假设自己从未使用过数据库。在一个数据库中能做的事情在另一个数据库中可能没有必要做,或者根本不能做。
在Oracle中,你会了解到:
l 事务是每个数据库的核心,它们是“好东西”。
l 应该延迟到适当的时刻才提交。不要太快提交,以避免对系统带来压力。这是因为,如果事务很长或很大,一般不会对系统有压力。相应的原则是:在必要时才提交,但是此前不要提交。事务的大小只应该根据业务逻辑来定。
l 只要需要,就应该尽可能长时间地保持对数据所加的锁。这些锁是你能利用的工具,而不是让你退避三舍的东西。锁不是稀有资源。恰恰相反,只要需要,你就应该长期地保持数据上的锁。锁可能并不稀少,而且它们可以防止其他会话修改信息。
l 在Oracle中,行级锁没有相关的开销,根本没有。不论你是有1个行锁,还是1 000 000个行锁,专用于锁定这个信息的“资源”数都是一样的。当然,与修改1行相比,修改1 000 000行要做的工作肯定多得多,但是对1 000 000行锁定所需的资源数与对1行锁定所需的资源数完全相同,这是一个固定的常量。
l 不要以为锁升级“对系统更好”(例如,使用表锁而不是行锁)。在Oracle中,锁升级(lock escalate)对系统没有任何好处,不会节省任何资源。也许有时会使用表锁,如批处理中,此时你很清楚会更新整个表,而且不希望其他会话锁定表中的行。但是使用表锁绝对不是为了避免分配行锁,想以此来方便系统。
l 可以同时得到并发性和一致性。每次你都能快速而准确地得到数据。数据读取器不会被数据写入器阻塞。数据写入器也不会被数据读取器阻塞。这是Oracle与大多数其他关系数据库之间的根本区别之一。
6.2 锁定问题
6.2.1 丢失更新
6.2.2 悲观锁定
悲观锁定(pessimistic locking)仅用于有状态(stateful)或有连接(connected)环境,也就是说,你的应用与数据库有一条连续的连接,而且至少在事务生存期中只有你一个人使用这条连接。除了简单地查询值并验证数据尚未修改外,我们要使用FOR UPDATE NOWAIT锁定这一行。
6.2.3 乐观锁定
第二种方法称为乐观锁定(optimistic locking),即把所有锁定都延迟到即将执行更新之前才做。
1. 使用版本列的乐观锁定
这是一个简单的实现,如果你想保护数据库表不出现丢失更新问题,应对每个要保护的表增加一列。这一列一般是 NUMBER 或DATE/TIMESTAMP 列,通常通过表上的一个行触发器来维护。每次修改行时,这个触发器要负责递增 NUMBER 列中的值,或者更新DATE/TIMESTAMP列。
2. 使用校验和的乐观锁定
这与前面的版本列方法很相似,不过在此要使用基数据本身来计算一个“虚拟的”版本列。
3. 使用 ORA_ROWSCN 的乐观锁定
从 Oracle 10g Release 1开始,你还可以使用内置的 ORA_ROWSCN 函数。它的工作与前面所述的版本列技术很相似,但是可以由Oracle自动执行,而不需要在表中增加额外的列,也不需要额外的更新/维护代码来更新这个值。除非你创建表时支持在行级维护ORA_ROWSCN,否则Oracle会在块级维护。也就是说,默认情况下,一个块上的多行会共享相同的 ORA_ROWSCN 值。如果更新一个块上的某一行,而且这个块上还有另外50行,那么这些行的ORA_ROWSCN也会推进。这往往会导致许多假警报,你认为某一行已经修改,但实际上它并没有改动。如果要在行级上维护则需要在建表时加上ROWDEPENDENCIES子句。
另外可以把SCN转换为墙上时钟,但有一些限制:数据库的正常运行时间只有5天左右。如:select scn_to_timestamp(ora_rowscn) from dept;
6.2.4 乐观锁定还是悲观锁定?
6.2.5 阻塞
如果一个会话持有某个资源的锁,而另一个会话在请求这个资源,就会出现阻塞(blocking)。这样一来,请求的会话会被阻塞,它会“挂起”,直至持有锁的会话放弃锁定的资源。几乎在所有情况下,阻塞都是可以避免的。实际上,如果你真的发现会话在一个交互式应用中被阻塞,就说明很有可能同时存在着另一个 bug,即丢失更新,只不过你可能没有意识到这一点。也就是说,你的应用逻辑有问题,这才是阻塞的根源。
数据库中有5条常见的DML语句可能会阻塞,具体是:INSERT、UPDATE、DELETE、MERGE和SELECT FOR UPDATE。
1. 阻塞的 INSERT
最常见的情况是,你有一个带主键的表,或者表上有惟一的约束,但有两个会话试图用同样的值插入一行。
一种解决方法是:创建一个触发器,在插入前,把插入行的主键散列到一个数字上,然后用一个排他锁把这个数字锁住,这样另一个人在插入相同的行时得不到这个锁,而收到一个错误,从而得知正试图插入相同行。
最后,还要记住,尽管Oracle有无限多个行级锁,但是 enqueue锁(这是一种队列锁)的个数则是有限的。如果在会话中插入大量行,而没有提交,可能就会发现创建了太
多的 enqueue 队列锁,而耗尽了系统的队列资源(超出了 ENQUEUE_RESOURCES 系统参数设置的最大值),因为每行都会创建另一个enqueue锁。如果确实发生了这种情况,就需要增大ENQUEUE_RESOURCES参数的值。还可以向触发器增加一个标志,允许打开或关闭这种检查。例如,如果我准备插入数百条或数千条记录,可能就不希望启用这个检查。
问题:什么是enqueue锁?为什么插入时每行都会创建另一个enqueue锁?ENQUEUE_RESOURCES 系统参数是个什么参数?
2. 阻塞的 Merge、Update 和 Delete
在一个交互式应用中,可以从数据库查询某个数据,允许最终用户处理这个数据,再把它“放回”到数据库中,此时如果 UPDATE 或DELETE阻塞,就说明你的代码中可能存在一个丢失更新问题(如果真是这样,按我的说法,就是你的代码中存在bug) 。你试图UPDATE
(更新)其他人正在更新的行(换句话说,有人已经锁住了这一行)。通过使用SELECT FOR UPDATE NOWAIT查询可以避免这个问题,这个查询能做到:
l 验证自从你查询数据之后数据未被修改(防止丢失更新)。
l 锁住行(防止UPDATE或DELETE被阻塞)。
6.2.6 死锁
根据我的经验,导致死锁的头号原因是外键未加索引(第二号原因是表上的位图索引遭到并发更新,这个内容将在第 11 章讨论)。
在以下两种情况下,Oracle在修改父表后会对子表加一个全表锁:
l 如果更新了父表的主键(倘若遵循关系数据库的原则,即主键应当是不可变的,这种情况就很少见),由于外键上没有索引,所以子表会被锁住。
l 如果删除了父表中的一行,整个子表也会被锁住(由于外键上没有索引)。
在以下情况下,未加索引的外键也可能带来问题:
l 如果有ON DELETE CASCADE,而且没有对子表加索引:例如,EMP是DEPT的子表,DELETE DEPTNO = 10应该CASCADE(级联)至 EMP[4]。如果 EMP中的 DEPTNO没有索引,那么删除 DEPT 表中的每一行时都会对 EMP 做一个全表扫描。这个全表扫描可能是不必要的,而且如果从父表删除多行,父表中每删除一行就要扫描一次子表。
l 从父表查询子表:再次考虑 EMP/DEPT 例子。利用 DEPTNO 查询 EMP 表是相当常见的。如果频繁地运行以下查询(例如,生成一个报告) ,你会发现没有索引会使查询速度变慢:
select * from dept, emp where emp.deptno = dept.deptno and dept.deptno = :X;
那么,什么时候不需要对外键加索引呢?答案是,一般来说,当满足以下条件时不需要加索引:
l 没有从父表删除行。
l 没有更新父表的惟一键/主键值(当心工具有时会无意地更新主键!)。
l 没有从父表联结子表(如DEPT联结到EMP)。
如果满足上述全部 3 个条件,那你完全可以跳过索引,不需要对外键加索引。如果满足以上的某个条件,就要当心加索引的后果。这是一种少有的情况,即Oracle“过分地锁定了”数据。
6.2.7 锁升级
如果数据库认为锁是一种稀有资源,而且想避免锁的开销,这些数据库中就会频繁使用锁升级。如果数据库支持锁升级,就说明这个数据库的锁定机制中存在某些内部开销,而且管理数百个锁需要做大量的工作。在Oracle中,1个锁的开销与1 000 000个锁是一样的,都没有开销。
注意:Oracle不会升级锁,从来不会。但是它会执行锁转换(lock conversion)或锁提升(lock promotion) ,这些词通常会与锁升级混淆。“锁转换”和“锁提升”是同义词。Oracle一般称这个过程为“锁转换”。
Oracle会尽可能地在最低级别锁定(也就是说,限制最少的锁),如果必要,会把这个锁转换为一个更受限的级别。