《oracle 专家高级编程 中文第六章.pdf》由会员分享,可在线阅读,更多相关《oracle 专家高级编程 中文第六章.pdf(33页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、 1第 6 章 锁 开发多用户、数据库驱动的应用时,最大的难点之一是:一方面要力争取得最大限度的并发访问,与此同时还要确保每个用户能以一致的方式读取和修改数据。为此就有了锁定(locking)机制,这也是所有数据库都具有的一个关键特性,Oracle 在这方面更是技高一筹。不过,Oracle 的这些特性的实现是 Oracle 所特有的,就像 SQL Server 的实现只是 SQL Server 特有的一样,应用执行数据处理时,要正确地使用这些机制,而这一点要由你(应用的开发人员)来保证。如果你做不到,你的应用可能会表现得出人意料,而且不可避免地会危及数据的完整性(见第 1 章的说明)。在这一章
2、中,我们将详细介绍 Oracle 如何对数据(例如,表中的行)和共享数据结构(如 SGA 中的内存结构)锁定。这里会分析Oracle 以怎样的粒度锁定数据,并指出这对你来说意味着什么。在适当的时候,我会把 Oracle 的锁定机制与其他流行的锁实现(即其他数据库中的锁定机制)进行对照比较,主要是为了消除关于行级锁的一个“神话”:人们认为行级锁总会增加开销;而实际上,在不同的实现中情况有所不同,只有当实现本身会增加开销时,行级锁才会增加开销。在下一章中,我们还会继续讨论这个内容,进一步研究 Oracle 的多版本技术,并说明锁定策略与多版本技术有什么关系。6.1 什么是锁?锁(lock)机制用于
3、管理对共享资源的并发访问。注意,我说的是“共享资源”而不是“数据库行”。Oracle 会在行级对表数据锁定,这固然不错,不过 Oracle 也会在其他多个级别上使用锁,从而对多种不同的资源提供并发访问。例如,执行一个存储过程时,过程本身会以某种模式锁定,以允许其他用户执行这个过程,但是不允许另外的用户以任何方式修改这个过程。数据库中使用锁是为了支持对共享资源进行并发访问,与此同时还能提供数据完整性和一致性。在单用户数据库中,并不需要锁。根据定义,只有一个用户修改信息。不过,如果有多个用户访问和修改数据或数据结构,就要有一种机制来防止对同一份信息的并发修改,这一点至关重要。这正是锁定所要做的全部
4、工作。需要了解的重要一点是:有多少种数据库,其中就可能有多少种实现锁定的方法。你可能对某个特定的关系数据库管理系统(relational database management system,RDBMS)的锁定模型有一定的经验,但凭此并不意味着你通晓锁定的一切。例如,在我“投身”Oracle 之前,曾经使用过许多其他的数据库,如 Sybase、Microsoft SQL Server 和 Informix。这 3 个数据库都为并发控制提供了锁定机制,但是每个数据库中实现锁定的方式都大相径庭。为了说明这一点,下面简要地概括一下我的“行进路线”,告诉你我是怎样从 SQL Server 开发人员发展
5、为Informix 用户,最后又怎样成为 Oracle 开发人员。那是好多年前的事情了,可能有些 SQL Server 支持者会说:“但是我们现在也有行级锁了!”没错,SQL Server 现在确实可以使用行级锁,但是其实现方式与 Oracle 中的实现方式完全不同。它们就像是苹果和桔子,是截然不同的两个物体,这正是关键所在。作为 SQL Server 程序员,我很少考虑多个用户并发地向表中插入数据的可能性。在 SQL Server 数据库中,这种情况极少发生。那时,SQL Server 只提供页级锁,对于非聚簇表,由于所有数据都会插入到表的最后一页,所以两个用户并发插入的情况根本不可能发生。
6、注意注意 从某种程度上讲,SQL Server 聚簇表(有一个聚簇索引的表)与 Oracle 聚簇有点相似,但二者存在很大的差别。SQL Server 以前只支持页(块)级锁,如果所插入的每一行都会插入到表的“末尾”,那么这个数据库中绝对不会有并发插入和并发事务。利用 SQL Server 中的聚簇索引,就能按聚簇键的顺序在整个表中插入行(而不是只在表的末尾插入),这就能改善 SQL Server 数据库的并发性。并发更新也存在同样的问题(因为 UPDATE 实际上就是 DELETE 再加上一个 INSERT)。可能正是由于这个原因,默认情况下,SQL Server 每执行一条语句后就会立即提
7、交或回滚。这样做的目的是为了得到更大的并发性,但是会破坏事务完整性。因此,大多数情况下,如果采用页级锁,多个用户就不能同时修改同一个表。另外,如果正在修改一个表,这会有效地阻塞对这个表的多个查询。如果我想查询一个表,而且所需的页被一个更新锁住了,那我就必须等待(等待,再等待)。这种锁定机制太糟糕了,要想支持耗时超过 1 秒的事务,结果可能是致命的;倘若真的这样做了,整个数据库看上去可能就像是“冻住”了一样。我从这里学到了很多坏习惯。我认识到:事务很“不好”,应该尽快地提交,而且永远不要持有数据的锁。并发性要以一致性为代价。要么保证正确,要么保证速度,在我看来,鱼和熊掌不可兼得。等我转而使用 I
8、nformix 之后,情况好了一些,但也不是太好。只要创建表时记得启用行级锁,就能允许两个人同时向这个表中插入数据。遗憾的是,这种并发性的代价很高。Informix 实现中的行级锁开销很大,不论从时间上讲还是从内存上讲都是如此。它要花时间获得和“不要”(或释放)这些行级锁,而且每个锁都要占用实际内存。另外,在启动数据库之前,必须计算系统可用的锁的总数。如果超过 2这个数,那你可就要倒霉了。由于这些原因,大多数表都采用页级锁创建,而且与 SQL Server 一样,Informix 中的行级锁和页级锁都会阻塞查询。所以,我再一次发现需要尽快地提交。在 SQL Server 中学到的坏习惯在此得到
9、了巩固,而且,我还学会了一条:要把锁当成一种很稀有的资源,一种可望而难求的事物。我了解到,应该手动地将行级锁升级为表级锁,从而尽量避免需要太多的锁而导致系统崩溃,我就曾经因此多次使系统崩溃。等开始使用 Oracle 时,我没有费心去读手册,看看这个特定的数据库中锁定是怎么工作的。毕竟,我用数据库已经不是一年半载了,而且也算得上是这个领域的专家(除了 Sybase、SQL Server 和 Informix,我还用过 Ingres、DB2、Gupta SQLBase 和许多其他的数据库)。我落入了过于自信的陷阱,自以为知道事情应该怎么做,所以想当然地认为事情肯定就会这样做。这一次我可大错特错了。
10、直到一次基准测试时,我才认识到犯了多大的错误。在这些数据库的早期阶段(大约 1992/1993 年),开发商常常对确实很大的数据库产品进行“基准测试”,想看看哪一个数据库能最快、最容易地完成工作,而且能提供最丰富的特性。这个基准测试在 Informix、Sybase、SQL Server 和 Oracle 之间进行。最先测试的是 Oracle。他们的技术人员来到现场,读过基准测试规范后,他们开始进行设置。我首先注意到的是,Oracle 技术人员只使用一个数据库表来记录他们的计时信息,尽管我们要建立数十条连接来执行测试,而且每条连接都需要频繁地向这个日志表中插入和更新数据。不仅如此,他们还打算在
11、基准测试期间读这个日志表!出于好心,我把一位 Oracle 技术人员叫到一边,问他这样做是不是疯了,为什么还要故意往系统里引入竞争呢?基准测试进程难道不是对这个表串行地执行操作吗?别人正在对表做大量的修改,而此时他们还要读这个表,基准测试不会因此阻塞吗?为什么他们想引入所有这些额外的锁,要知道这些锁都需要他们来管理呀!我有一大堆“为什么你会那么想?”之类的问题。那时,我认为 Oracle 的技术人员有点傻。也就是说,直到我摆脱 SQL Server 或 Informix 的阴影,显示了让两个人同时插入一个表会有什么结果时;或者有人试图查询一个表,而其他人正在向这个表中插入行,此时会有什么结果(
12、查询将每秒返回 0 行),我的观念才有所转变。Oracle 的做法与几乎所有其他数据库的做法有显著的差别,简直是天壤之别。不用说,无论是 Informix 还是 SQL Server 技术人员,都对这种数据库日志表方法不太热心。他们更倾向于把计时信息记录到操作系统上的平面文件中。Oracle 人员对于如何胜出 SQL Server 和 Informix 很有自己的一套:他们只是问测试人员:“如果数据已经锁定,你当前的数据库每秒返回多少行?”并以此为出发点“展开攻势”。从这个故事得到的教训是两方面的。首先,所有数据库本质上都不同。其次,为一个新的数据库平台设计应用时,对于数据库如何工作不能做任何
13、假设。学习每一个新数据库时,应该假设自己从未使用过数据库。在一个数据库中能做的事情在另一个数据库中可能没有必要做,或者根本不能做。在 Oracle 中,你会了解到:q 事务是每个数据库的核心,它们是“好东西”。q 应该延迟到适当的时刻才提交。不要太快提交,以避免对系统带来压力。这是因为,如果事务很长或很大,一般不会对系统有压力。相应的原则是:在必要时才提交,但是此前不要提交。事务的大小只应该根据业务逻辑来定。q 只要需要,就应该尽可能长时间地保持对数据所加的锁。这些锁是你能利用的工具,而不是让你退避三舍的东西。锁不是稀有资源。恰恰相反,只要需要,你就应该长期地保持数据上的锁。锁可能并不稀少,而
14、且它们可以防止其他会话修改信息。q 在 Oracle 中,行级锁没有相关的开销,根本没有。不论你是有 1 个行锁,还是 1 000 000 个行锁,专用于锁定这个信息的“资源”数都是一样的。当然,与修改 1 行相比,修改 1 000 000 行要做的工作肯定多得多,但是对 1 000 000 行锁定所需的资源数与对1 行锁定所需的资源数完全相同,这是一个固定的常量。q 不要以为锁升级“对系统更好”(例如,使用表锁而不是行锁)。在 Oracle 中,锁升级(lock escalate)对系统没有任何好处,不会节省任何资源。也许有时会使用表锁,如批处理中,此时你很清楚会更新整个表,而且不希望其他会
15、话锁定表中的行。但是使用表锁绝对不是为了避免分配行锁,想以此来方便系统。q 可以同时得到并发性和一致性。每次你都能快速而准确地得到数据。数据读取器不会被数据写入器阻塞。数据写入器也不会被数据读取器阻塞。这是 Oracle 与大多数其他关系数据库之间的根本区别之一。接下来在这一章和下一章的介绍中,我还会强调这几点。6.2 锁定问题 讨论 Oracle 使用的各种类型的锁之前,先了解一些锁定问题会很有好处,其中很多问题都是因为应用设计不当,没有正确地使用(或 3者根本没有使用)数据库锁定机制产生的。6.2.1 丢失更新 丢失更新(lost update)是一个经典的数据库问题。实际上,所有多用户计
16、算机环境都存在这个问题。简单地说,出现下面的情况时(按以下所列的顺序),就会发生丢失更新:(1)会话 Session1 中的一个事务获取(查询)一行数据,放入本地内存,并显示给一个最终用户 User1。(2)会话 Session2 中的另一个事务也获取这一行,但是将数据显示给另一个最终用户 User2。(3)User1 使用应用修改了这一行,让应用更新数据库并提交。会话 Session1 的事务现在已经执行。(4)User2 也修改这一行,让应用更新数据库并提交。会话 Session2 的事务现在已经执行。这个过程称为“丢失更新”,因为第(3)步所做的所有修改都会丢失。例如,请考虑一个员工更新
17、屏幕,这里允许用户修改地址、工作电话号码等信息。应用本身非常简单:只有一个很小的搜索屏幕要生成一个员工列表,然后可以搜索各位员工的详细信息。这应该只是小菜一碟。所以,编写应用程序时没有考虑锁定,只是简单的 SELECT 和 UPDATE 命令。然后最终用户(User1)转向详细信息屏幕,在屏幕上修改一个地址,单击 Save(保存)按钮,得到提示信息称更新成功。还不错,但是等到 User1 第二天要发出一个税表时,再来检查记录,会发现所列的还是原先的地址。到底出了什么问题?很遗憾,发生这种情况太容易了。在这种情况下,User1 查询记录后,紧接着另一位最终用户(User2)也查询了同一条记录;也
18、就是说,在 User1 读取数据之后,但在她修改数据之前,User2 也读取了这个数据。然后,在 User2 查询数据之后,User1 执行了更新,接到成功信息,甚至还可能再次查询看看是否已经修改。不过,接下来 User2 更新了工作电话号码字段,并单击 Save(保存)按钮,完全不知道他已经用旧数据重写(覆盖)了 User1 对地址字段的修改!之所以会造成这种情况,这是因为应用开发人员编写的程序是这样的:更新一个特定的字段时,该记录的所有字段都会“刷新”(只是因为更新所有列更容易,这样就不用先得出哪些列已经修改,并且只更新那些修改过的列)。可以注意到,要想发生这种情况,User1 和 Use
19、r2 甚至不用同时处理记录。他们只要在大致同一时间处理这个记录就会造成丢失更新。我发现,如果 GUI 程序员在数据库方面的培训很少(或者没有),编写数据库应用程序时就时常会冒出这个数据库问题。这些程序员了解了如何使用 SELECT、INSERT、UPDATE 和 DELETE 等语句后,就着手开始编写应用程序。如果开发出来的应用程序有上述表现,就会让用户完全失去对它的信心,特别是这种现象只是随机地、零星地出现,而且在受控环境中完全不可再生(这就导致开发人员误以为是用户的错误)。许多工具可以保护你避免这种情况,如 Oracle Forms 和 HTML DB,这些工具能确保:从查询记录的那个时刻
20、开始,这个记录没有改变,而且对它执行任何修改时都会将其锁定,但是其他程序做不到这一点(如手写的 Visual Basic 或 Java 程序)。为了保护你不丢失更新,这些工具在后台做了哪些工作呢?或者说开发人员必须自己做哪些工作呢?实际上就是要使用某种锁定策略,共有两种锁定策略:悲观锁定或乐观锁定。6.2.2 悲观锁定 用户在屏幕上修改值之前,这个锁定方法就要起作用。例如,用户一旦有意对他选择的某个特定行(屏幕上可见)执行更新,如单击屏幕上的一个按钮,就会放上一个行锁。悲观锁定(pessimistic locking)仅用于有状态(stateful)或有连接(connected)环境,也就是说
21、,你的应用与数据库有一条连续的连接,而且至少在事务生存期中只有你一个人使用这条连接。这是 20 世纪 90 年代中期客户/服务器应用中的一种流行做法。每个应用都得到数据库的一条直接连接,这条连接只能由该应用实例使用。这种采用有状态方式的连接方法已经不太常见了(不过并没有完全消失),特别是随着 20 世纪 90 年代中后期应用服务器的出现,有状态连接更是少见。假设你在使用一条有状态连接,应用可以查询数据而不做任何锁定:scottORA10G select empno,ename,sal from emp where deptno=10;EMPNO ENAME SAL -7782 CLARK 24
22、50 7839 KING 5000 4 7934 MILLER 1300 最后,用户选择他想更新的一行。在这个例子中,假设用户选择更新 MILLER 行。在这个时间点上(即用户还没有在屏幕上做任何修改,但是行已经从数据库中读出一段时间了),应用会绑定用户选择的值,从而查询数据库,并确保数据尚未修改。在 SQL*Plus 中,为了模拟应用可能执行的绑定调用,可以发出以下命令:scottORA10G variable empno number scottORA10G variable ename varchar2(20)scottORA10G variable sal number scottOR
23、A10G exec:empno:=7934;:ename:=MILLER;:sal:=1300;PL/SQL procedure successfully completed.下面,除了简单地查询值并验证数据尚未修改外,我们要使用 FOR UPDATE NOWAIT 锁定这一行。应用要执行以下查询:scottORA10G 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
24、 1300 根据屏幕上输入的数据,应用将提供绑定变量的值(在这里就是 7934、MILLER 和 1300),然后重新从数据库查询这一行,这一次会锁定这一行,不允许其他会话更新;因此,这种方法称为悲观锁定(pessimistic locking)。在试图更新之前我们就把行锁住了,因为我们很悲观,对于这一行能不能保持未改变很是怀疑。所有表都应该有一个主键(前面的 SELECT 最多会获取一个记录,因为它包括主键 EMPNO),而且主键应该是不可变的(不应更新主键),从这句话可以得出三个结论:q 如果底层数据没有改变,就会再次得到 MILLER 行,而且这一行会被锁定,不允许其他会话更新(但是允许
25、其他会话读)。q 如果另一个用户正在更新这一行,我们就会得到一个 ORA-00054:resource busy(ORA-00054:资源忙)错误。相应地,必须等待更新这一行的用户执行工作。q 在选择数据和指定有意更新之间,如果有人已经修改了这一行,我们就会得到 0 行。这说明,屏幕上的数据是过时的。为了避免前面所述的丢失更新情况,应用需要重新查询(requery),并在允许在最终用户修改之前锁定数据。有了悲观锁定,User2 试图更新电话号码字段时,应用现在会识别出地址字段已经修改,所以会重新查询数据。因此,User2 不会用这个字段的旧数据覆盖User1 的修改。一旦成功地锁定了这一行,应
26、用就会绑定新值,发出更新命令后,提交所做的修改:scottORA10G update emp 2 set ename=:ename,sal=:sal 3 where empno=:empno;1 row updated.scottORA10G commit;Commit complete.现在可以非常安全地修改这一行。我们不可能覆盖其他人所做的修改,因为已经验证了在最初读出数据之后以及对数据锁定之前数据没有改变。56.2.3 乐观锁定 第二种方法称为乐观锁定(optimistic locking),即把所有锁定都延迟到即将执行更新之前才做。换句话说,我们会修改屏幕上的信息而不要锁。我们很乐观,
27、认为数据不会被其他用户修改;因此,会等到最后一刻才去看我们的想法对不对。这种锁定方法在所有环境下都行得通,但是采用这种方法的话,执行更新的用户“失败”的可能性会加大。这说明,这个用户要更新他的数据行时,发现数据已经修改过,所以他必须从头再来。可以在应用中同时保留旧值和新值,然后在更新数据时使用如下的更新语句,这是乐观锁定的一种流行实现:Update table Set column1=:new_column1,column2=:new_column2,.Where primary_key=:primary_key And column1=:old_column1 And column2=:ol
28、d_column2.在此,我们乐观地认为数据没有修改。在这种情况下,如果更新语句更新了一行,那我们很幸运;这说明,在读数据和提交更新之间,数据没有改变。但是如果更新了零行,我们就会失败;另外一个人已经修改了数据,现在我们必须确定应用中下一步要做什么。是让最终用户查询这一行现在的新值,然后再重新开始事务呢(这可能会让用户很受打击,因为这一行有可能又被修改了)?还是应该根据业务规则解决更新冲突,试图合并两个更新的值(这需要大量的代码)?实际上,前面的 UPDATE 能避免丢失更新,但是确实有可能被阻塞,在等待另一个会话执行对这一行的 UPDATE 时,它会挂起。如果所有应用(会话)都使用乐观锁定,
29、那么使用直接的 UPDATE 一般没什么问题,因为执行更新并提交时,行只会被锁定很短的时间。不过,如果某些应用使用了悲观锁定,它会在一段相对较长的时间内持有行上的锁,你可能就会考虑使用 SELECT FOR UPDATE NOWAIT,以此来验证行是否未被修改,并在即将 UPDATE 之前锁定来避免被另一个会话阻塞。实现乐观并发控制的方法有很多种。我们已经讨论了这样的一种方法,即应用本身会存储行的所有“前”(before)映像。在后几节中,我们将介绍另外三种方法,分别是:q 使用一个特殊的列,这个列由一个数据库触发器或应用程序代码维护,可以告诉我们记录的“版本”q 使用一个校验和或散列值,这是
30、使用原来的数据计算得出的 q 使用新增的 Oracle 10g 特性 ORA_ROWSCN。1.使用版本列的乐观锁定使用版本列的乐观锁定 这是一个简单的实现,如果你想保护数据库表不出现丢失更新问题,应对每个要保护的表增加一列。这一列一般是 NUMBER 或DATE/TIMESTAMP 列,通常通过表上的一个行触发器来维护。每次修改行时,这个触发器要负责递增 NUMBER 列中的值,或者更新DATE/TIMESTAMP 列。如果应用要实现乐观并发控制,只需要保存这个附加列的值,而不需要保存其他列的所有“前”映像。应用只需验证请求更新那一刻,数据库中这一列的值与最初读出的值是否匹配。如果两个值相等
31、,就说明这一行未被更新过。下面使用 SCOTT.DEPT 表的一个副本来看看乐观锁定的实现。我们可以使用以下数据定义语言(Data Definition Language,DDL)来创建这个表:ops$tkyteORA10G create table dept 2 (deptno number(2),3 dname varchar2(14),4 loc varchar2(13),5 last_mod timestamp with time zone 6 default systimestamp 7 not null,8 constraint dept_pk primary key(deptno
32、)69 )10/Table created.然后向这个表 INSERT(插入)DEPT 数据的一个副本:ops$tkyteORA10G insert into dept(deptno,dname,loc)2 select deptno,dname,loc 3 from scott.dept;4 rows created.ops$tkyteORA10G commit;Commit complete.以上代码会重建 DEPT 表,但是将有一个附加的 LAST_MOD 列,这个列使用 TIMESTAMP WITH TIME ZONE 数据类型(Oracle9i 及以上版本中才有这个数据类型)。我们将
33、这个列定义为 NOT NULL,以保证这个列必须填有数据,其默认值是当前的系统时间。这个 TIMESTAMP 数据类型在 Oracle 中精度最高,通常可以精确到微秒(百万分之一秒)。如果应用要考虑到用户的思考时间,这种 TIMESTAMP 级的精度实在是绰绰有余,而且数据库获取一行后,人看到这一行,然后修改,再向数据库发回更新,一般不太可能在不到 1 秒钟的片刻时间内执行整个过程。两个人在同样短的时间内(不到 1 秒钟)读取和修改同一行的几率实在太小了。接下来,需要一种方法来维护这个值。我们有两种选择:可以由应用维护这一列,更新记录时将 LAST_MOD 列的值设置为SYSTIMESTAMP
34、;也可以由触发器/存储过程来维护。如果让应用维护 LAST_MOD,这比基于触发器的方法表现更好,因为触发器会代表 Oracle 对修改增加额外的处理。不过这并不是说:无论什么情况,你都要依赖所有应用在表中经过修改的所有位置上一致地维护LAST_MOD。所以,如果要由各个应用负责维护这个字段,就需要一致地验证 LAST_MOD 列未被修改,并把 LAST_MOD 列设置为当前的 SYSTIMESTAMP。例如,如果应用查询 DEPTNO=10 这一行:ops$tkyteORA10G variable deptno number ops$tkyteORA10G variable dname va
35、rchar2(14)ops$tkyteORA10G variable loc varchar2(13)ops$tkyteORA10G variable last_mod varchar2(50)ops$tkyteORA10G 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 procedure successfully completed.目前我们看到的是:ops$tkyteORA10G sel
36、ect:deptno dno,:dname dname,:loc loc,:last_mod lm 2 from dual;DNO DNAME LOC LM-10 ACCOUNTING NEW YORK 25-APR-05 10.54.00.493380 AM-04:00 再使用下面的更新语句来修改信息。最后一行执行了一个非常重要的检查,以确保时间戳没有改变,并使用内置函数TO_TIMESTAMP_TZ(TZ 是 TimeZone 的缩写,即时区)将以上 select(选择)得到的串转换为适当的数据类型。另外,如果发现行已经更新,以下更新语句中的第 3 行会把 LAST_MOD 列更新为当前时
37、间:ops$tkyteORA10G update dept 2 set dname=initcap(:dname),73 last_mod=systimestamp 4 where deptno=:deptno 5 and last_mod=to_timestamp_tz(:last_mod);1 row updated.可以看到,这里更新了一行,也就是我们关心的那一行。在此按主键(DEPTNO)更新了这一行,并验证从最初读取记录到执行更新这段时间,LAST_MOD 列未被其他会话修改。如果我们想尝试再更新这个记录,仍然使用同样的逻辑,不过没有获取新的 LAST_MOD值,就会观察到以下情况:
38、ops$tkyteORA10G 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 rows updated.注意到这一次报告称“0 rows updated”(更新了 0 行),因为关于 LAST_MOD 的谓词条件不能满足。尽管 DEPTNO 10 还存在,但是想要执行更新的那个时刻的 LAST_MOD 值与查询行时的时间戳值不再匹配。所以,应用知道,既然未能修改行,就说明数据库中的数据
39、已经(被别人)改变,现在它必须得出下一步要对此做什么。不能总是依赖各个应用来维护这个字段,原因是多方面的。例如,这样会增加应用程序代码,而且只要是表中需要修改的地方,都必须重复这些代码,并正确地实现。在一个大型应用中,这样的地方可能很多。另外,将来开发的每个应用也必须遵循这些规则。应用程序代码中很可能会“遗漏”某一处,未能适当地使用这个字段。因此,如果应用程序代码本身不负责维护这个 LAST_MOD 字段,我相信应用也不应负责检查这个 LAST_MOD 字段(如果它确实能执行检查,当然也能执行更新!)。所以在这种情况下,我建议把更新逻辑封装到一个存储过程中,而不要让应用直接更新表。如果无法相信
40、应用能维护这个字段的值,那么也无法相信它能正确地检查这个字段。存储过程可以取以上更新中使用的绑定变量作为输入,执行同样的更新。当检测到更新了 0 行时,存储过程会向客户返回一个异常,让客户知道更新实际上失败了。还有一种实现是使用一个触发器来维护这个 LAST_MOD 字段,但是对于这么简单的工作,我建议还是避免使用触发器,而让 DML来负责。触发器会引入大量开销,而且在这种情况下没有必要使用它们。2.使用校验和的乐观锁定使用校验和的乐观锁定 这与前面的版本列方法很相似,不过在此要使用基数据本身来计算一个“虚拟的”版本列。为了帮助解释有关校验和或散列函数的目标和概念,以下引用了 Oracle 1
41、0g PL/SQL Supplied Packages Guide 中的一段话(尽管现在还没有介绍如何使用 Oracle 提供的任何一个包!):单向散列函数取一个变长输入串(即数据),并把它转换为一个定长的输出串(通常更小),这个输出称为散列值(hash value)。散列值充当输入数据的一个惟一标识符(就像指纹一样)。可以使用散列值来验证数据是否被修改。需要注意,单向散列函数只能在一个方向上应用。从输入数据计算散列值很容易,但是要生成能散列为某个特定值的数据却很难。散列值或校验和并非真正惟一。只能说,通过适当地设计,能使出现冲突的可能性相当小,也就是说,两个随机的串有相同校验和或散列值的可能
42、性极小,足以忽略不计。与使用版本列的做法一样,我们可以采用同样的方法使用这些散列值或校验和,只需把从数据库读出数据时得到的散列或校验和值与修改数据前得到的散列或校验和值进行比较。在我们读出数据之后,但是在修改数据之前,如果有人在这段时间内修改了这一行的值,散列值或校验和值往往会大不相同。有很多方法来计算散列或校验和。这里列出其中的 3 种方法,分别在以下 3 个小节中介绍。所有这些方法都利用了 Oracle 提供的数据库包:q OWA_OPT_LOCK.CHECKSUM:这个方法在 Oracle8i 8.1.5 及以上版本中提供。给定一个串,其中一个函数会返回一个 16 位的校验和。给定 RO
43、WID 时,另一个函数会计算该行的 16 位校验和,而且同时将这一行锁定。出现冲突的可能性是 65 536 分之一(65 536 个串中有一个冲突,这是假警报的最大几率)。q DBMS_OBFUSCATION_TOOLKIT.MD5:这个方法在 Oracle8i 8.1.7 及以上版本中提供。它会计算一个 128 位的消息摘要。冲突的可能性是 3.4028E+38 分之一(非常小)。8q DBMS_CRYPTO.HASH:这个方法在 Oracle 10g Release 1 及以上版本中提供。它能计算一个 SHA-1(安全散列算法 1,Secure Hash Algorithm 1)或 MD4
44、/MD5 消息摘要。建议你使用 SHA-1 算法。注意注意 很多编程语言中都提供了一些散列和校验和函数,所以还可以使用数据库之外的散列和校验和函数。下面的例子显示了如何使用 Oracle 10g 中的 DBMS_CRYPTO 内置包来计算这些散列/校验和。这个技术也适用于以上所列的另外两个包;逻辑上差别不大,但是调用的 API可能不同。下面在某个应用中查询并显示部门 10 的信息。查询信息之后,紧接着我们使用 DBMS_CRYPTO 包计算散列。这是应用中要保留的“版本”信息:ops$tkyteORA10G begin 2 for x in(select deptno,dname,loc 3
45、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:C44F70526
46、61CE945D385D5C3F911E70FA99407A6 PL/SQL procedure successfully completed.可以看到,散列值就是一个很大的 16 进制位串。DBMS_CRYPTO 的返回值是一个 RAW 变量,显示时,它会隐式地转换为 HEX。这个值会在更新前使用。为了执行更新,需要在数据库中获取这一行,并按其现在的样子锁定,然后计算所获取的行的散列值,将这个新散列值与从数据库读出数据时计算的散列值进行比较。上述逻辑表示如下(当然,在实际中,可能使用绑定变量而不是散列值直接量):ops$tkyteORA10G begin 2 for x in(select
47、deptno,dname,loc 3 from dept 4 where deptno=10 5 for update nowait)6 loop 7 if(hextoraw(C44F7052661CE945D385D5C3F911E70FA99407A6)8 dbms_crypto.hash 9 (utl_raw.cast_to_raw(x.deptno|/|x.dname|/|x.loc),10 dbms_crypto.hash_sh1)11 then 12 raise_application_error(-20001,Row was modified);13 end if;14 end
48、loop;15 update dept 16 set dname=lower(dname)17 where deptno=10;18 commit;19 end;920/PL/SQL procedure successfully completed.更新后,重新查询数据,并再次计算散列值,此时可以看到散列值大不相同。如果有人抢在我们前面先修改了这一行,我们的散列值比较就不会成功:ops$tkyteORA10G begin 2 for x in(select deptno,dname,loc 3 from dept 4 where deptno=10)5 loop 6 dbms_output.p
49、ut_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:F3DE485922D44DF598C2CEBC34C27DD2216FB90F PL/SQL procedure s
50、uccessfully completed.这个例子显示了如何利用散列或校验和来实现乐观锁定。要记住,计算散列或校验和是一个 CPU 密集型操作(相当占用 CPU),其计算代价很昂贵。如果系统上 CPU 是稀有资源,在这种系统上就必须充分考虑到这一点。不过,如果从“网络友好性”角度看,这种方法会比较好,因为只需在网络上传输相当小的散列值,而不是行的完整的前映像和后映像(以便逐列地进行比较),所以消耗的资源会少得多。下面最后一个例子会使用一个新的 Oracle 10g 函数 ORA_ROWSCN,它不仅很小(类似于散列),而且计算时不是 CPU 密集的(不会过多占用 CPU)。3.使用使用 OR