且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

全面解析oracle中的锁机制1

更新时间:2022-06-06 22:03:38

1、锁的理解:

一般来说,以我现有的知识理解锁,可能认为锁是针对数据行,或者数据表的,但是oracle中对锁的适用范围是对所有oracle内的共享资源,比如,一个存储过程,一个触发器。。。当你在用这个存储过程的时候,就是给这个存储过程加了一个锁,别人可以用这个过程,但是不能修改它。

总结一下,Oracle使用了多种锁,包括:
         TX锁:    修改数据的事务在执行期间会获得这种锁(一看到TX锁,一般人会认为他是一个行级排他锁,但这个是不正确的,其实TX是一个事务锁,他无论处理了多少行,都只是这一个事务锁在起作用,在v$lock视图中这个锁的id1,id2(这两个值也同时告诉了你,这些数据在回滚段中的位置,这样当另一个事务来读取,却发现别人在用不能读,他就同个这个TX锁的id到回滚段中找到备份的数据读取出来)的值是不会变化的。这个事务锁会在行级对数据产生影响,比如阻塞。在一个主键表中,我们更新一个主键值是,就会产生一个TX事务锁,他阻止其他人对这一行进行更新。


    TM锁和DDL锁:    在你修改一个对象的内容(对于TM锁)或对象本身(对应DDL锁)时,这些锁可以确保对象的结构不被修改(TM锁:在这个锁上面没有阻塞和等待,他是一个表级共享锁,就是说每个用户可以以共享的方式(lmode为3)持有他,其实TM更像一个分段级锁,我们平时之所以叫他表级锁,是因为我们的表只有一个段,如果将表分为好几个段,每个段上都会加一个TM锁)。


    闩(latch):    这是Oracle的内部锁,用来协调对其共享数据结构的访问。


在oracle里面不会存在一个锁管理器,要是那样的话还得等待锁管理器分配锁或者释放锁,这样锁越多管理的开销越大,显然是很不合理的,在oracle数据库中,并不会对某个表或者某几行上加锁,锁是以数据块的一个属性存在的。也就是说每个数据块本身就存储着自己数据块中数据的信息这个地方叫ITL (interesed transaction list)凡是在这个数据块上有活动的事务,他的信息就会记录在这里面供后续的操作查询使用,以保证事务的一致性


事务是每个数据库的核心,它们是“好东西”。

应该延迟到适当的时刻才提交。不要太快提交,以避免对系统带来压力。这是因为,如果事务很长或很大,一般不会对系统有压力。相应的原则是:在必要时才提交,但是此前不要提交。事务的大小只应该根据业务逻辑来定。

只要需要,就应该尽可能长时间地保持对数据所加的锁。这些锁是你能利用的工具,而不是让你退避三舍的东西。锁不是稀有资源。恰恰相反,只要需要,你就应该长期地保持数据上的锁。锁可能并不稀少,而且它们可以防止其他会话修改信息。

在Oracle 中,行级锁没有相关的开销,根本没有。不论你是有1 个行锁,还是1 000 000个行锁,专用于锁定这个信息的“资源”数都是一样的。当然,与修改1 行相比,修改1 000 000行要做的工作肯定多得多,但是对1 000 000 行锁定所需的资源数与对1 行锁定所需的资源数完全相同,这是一个固定的常量。

不要以为锁升级“对系统更好”(例如,使用表锁而不是行锁)。在Oracle 中,锁升级(lockescalate)对系统没有任何好处,不会节省任何资源。也许有时会使用表锁,如批处理中,此时你很清楚会更新整个表,而且不希望其他会话锁定表中的行。但是使用表锁绝对不是为了避免分配行锁,想以此来方便系统。可以同时得到并发性和一致性。每次你都能快速而准确地得到数据。数据读取器不会被数据写入器阻塞。数据写入器也不会被数据读取器阻塞。这是Oracle 与大多数其他关系数据库之间的根本区别之一。


2、丢失更新问题的解决:悲观锁定和乐观锁定

为了得到最大的性能,一般数据库都有并发机制,不过带来的问题就是数据访问的冲突。为了解决这个问题,大多数数据库用的方法就是数据的锁定。

数据的锁定分为两种方法,第一种叫做悲观锁,第二种叫做乐观锁

什么叫悲观锁呢,悲观锁顾名思义,就是对数据的冲突采取一种悲观的态度,也就是说假设数据肯定会冲突,所以在数据开始读取的时候就把数据锁定住。

而乐观锁就是认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让用户返回错误的信息,让用户决定如何去做。

先从悲观锁开始说。在SqlServer等其余很多数据库中,数据的锁定通常采用页级锁的方式,也就是说对一张表内的数据是一种串行化的更新插入机制,在任何时间同一张表只会插1条数据,别的想插入的数据要等到这一条数据插完以后才能依次插入。带来的后果就是性能的降低,在多用户并发访问的时候,当对一张表进行频繁操作时,会发现响应效率很低,数据库经常处于一种假死状态。而Oracle用的是行级锁,只是对想锁定的数据才进行锁定,其余的数据不相干,所以在对Oracle表中并发插数据的时候,基本上不会有任何影响。

注:对于悲观锁是针对并发的可能性比较大,而一般在我们的应用中用乐观锁足矣。


Oracle的悲观锁需要利用一条现有的连接,分成两种方式,从SQL语句的区别来看,就是一种是for update,一种是for update nowait的形式。比如我们看一个例子。首先建立测试用的数据库表。

CREATE TABLE TEST

(ID,NAME,LOCATION,VALUE,CONSTRAINT test_pk PRIMARY KEY(ID))

AS SELECT deptno, dname, loc, 1

 FROM scott.dept


这里我们利用了Oracle的Sample的scott用户的表,把数据copy到我们的test表中。首先我们看一下for update锁定方式。首先我们执行如下的select for update语句。

select * from test where id = 10 for update

通过这条检索语句锁定以后,再开另外一个sql*plus窗口进行操作,再把上面这条sql语句执行一遍,你会发现sqlplus好像死在那里了,好像检索不到数据的样子,但是也不返回任何结果,就属于卡在那里的感觉。这个时候是什么原因呢,就是一开始的第一个Session中的select for update语句把数据锁定住了。由于这里锁定的机制是wait的状态(只要不表示nowait那就是wait),所以第二个Session(也就是卡住的那个sql*plus)中当前这个检索就处于等待状态。当第一个session最后commit或者rollback之后,第二个session中的检索结果就是自动跳出来,并且也把数据锁定住。不过如果你第二个session中你的检索语句如下所示。

select * from test where id = 10


也就是没有for update这种锁定数据的语句的话,就不会造成阻塞了。另外一种情况,就是当数据库数据被锁定的时候,也就是执行刚才for update那条sql以后,我们在另外一个session中执行for update nowait后又是什么样呢。比如如下的sql语句。 由于这条语句中是制定采用nowait方式来进行检索,所以当发现数据被别的session锁定中的时候,就会迅速返回ORA-00054错误,内容是资源正忙, 但指定以 NOWAIT 方式获取资源。所以在程序中我们可以采用nowait方式迅速判断当前数据是否被锁定中,如果锁定中的话,就要采取相应的业务措施进行处理。

select * from test where id = 10 for update nowait


那这里另外一个问题,就是当我们锁定住数据的时候,我们对数据进行更新和删除的话会是什么样呢。比如同样,我们让第一个Session锁定住id=10的那条数据,我们在第二个session中执行如下语句。

update test set value=2 where id = 10


这个时候我们发现update语句就好像select for update语句一样也停住卡在这里,当你第一个session放开锁定以后update才能正常运行。当你update运行后,数据又被你update语句锁定住了,这个时候只要你update后还没有commit,别的session照样不能对数据进行锁定更新等等。

总之,Oracle中的悲观锁就是利用Oracle的Connection对数据进行锁定。在Oracle中,用这种行级锁带来的性能损失是很小的,只是要注意程序逻辑,不要给你一不小心搞成死锁了就好。而且由于数据的及时锁定,在数据提交时候就不会出现冲突,可以省去很多恼人的数据冲突处理。缺点就是你必须要始终有一条数据库连接,就是说在整个锁定到最后放开锁的过程中,你的数据库联接要始终保持住。

与悲观锁相对的,我们有了乐观锁。乐观锁一开始也说了,就是一开始假设不会造成数据冲突,在最后提交的时候再进行数据冲突检测。在乐观锁中,我们有5种常用的做法来实现。稍后介绍这五种做法。

我们乐观得认为我们取得的数据没有被修改,但是,在执行对取得的数据进行一行修改的时候,有可能返回已修改一行,也有可能返回已修改0行,这就说明我们取来数据后有别的事务修改了这一行数据,现在往下要做的是:1、再次执行查询,把数据获取,让后重新执行这个事务(不过,在查询和修改之间又有可能一个新用户又修改了一下数据,这种事情在高度并发的数据库中是有可能发生的,这样一来,主人公会比较郁闷)。2、可以根据业务要求,用代码实现这两次更新的合并(这需要大量的代码,耗费较多的资源)


[1]第一种就是在数据取得的时候把整个数据都copy到应用中,在进行提交的时候比对当前数据库中的数据和开始的时候更新前取得的数据。当发现两个数据一模一样以后,就表示没有冲突可以提交,否则则是并发冲突,需要去用业务逻辑进行解决。

[2]第二种乐观锁的做法就是采用版本戳,这个在Hibernate中得到了使用。采用版本戳的话,首先需要在你有乐观锁的数据库table上建立一个新的column(这一列有数据库触发器或者应用程序代码维护控制,其实存储过程是个不错的选择,开销小,易于维护),比如为number,或者date/timestamp型当你数据每更新一次的时候,版本数就会往上增加1。比如同样有2个session同样对某条数据进行操作。两者都取到当前的数据的版本号为1,当第一个session进行数据更新后,在提交的时候查看到当前数据的版本还为1,和自己一开始取到的版本相同。就正式提交,然后把版本号增加1,这个时候当前数据的版本为2。当第二个session也更新了数据提交的时候,发现数据库中版本为2,和一开始这个session取到的版本号不一致,就知道别人更新过此条数据,这个时候再进行业务处理,比如整个Transaction都Rollback等等操作(这样用版本戳就避免了第一种方法的数据全部拷贝到应用中,节省时间空间或者资源)。在用版本戳的时候,可以在应用程序侧使用版本戳的验证,也可以在数据库侧采用Trigger(用存储过程更好)来进行验证。不过数据库的Trigger的性能开销还是比较的大,所以能在应用侧进行验证的话还是推荐不用Trigger。

如果无法相信应用能维护这个字段的值,那么也无法相信它能正确地检查这个字段。存储过程可以取以上更新中使用的绑定变量作为输入,执行同样的更新。当检测到更新了0 行时,存储过程会向客户返回一个异常,让客户知道更新实际上失败了。

[3]第三种做法和第二种做法有点类似,就是也新增一个Table的Column,不过这次这个column是采用timestamp型,存储数据最后更新的时间。在Oracle9i以后可以采用新的数据类型,也就是timestamp with time zone类型来做时间戳。这种Timestamp的数据精度在Oracle的时间类型中是最高的,精确到微秒(还没与到纳秒的级别),一般来说,加上数据库处理时间和人的思考动作时间,微秒级别是非常非常够了,其实只要精确到毫秒甚至秒都应该没有什么问题。和刚才的版本戳类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。如果不想把代码写在程序中或者由于别的原因无法把代码写在现有的程序中,也可以把这个时间戳乐观锁逻辑写在Trigger或者存储过程中。

[4]第四种做法跟以上两种也差不多,不过不在新建列了,使用行的数据计算一个hash值,把这个hash值作为这行数据的标志,若数据改变,则hash值一定会改变的,这样就能验证是否数据被改变了。

[5]从Oracle 10g Release 1 开始,你还可以使用内置的ORA_ROWSCN 函数。它的工作与前面所述的版本列技术很相似,但是可以由Oracle 自动执行,而不需要在表中增加额外的列,也不需要额外的更新/维护代码来更新这个值。

ORA_ROWSCN建立在oracle的内部时钟SCN号上的,,每次提交oracle SCN 号都会增加(当然别的修改也会使他增加,但是scn不会减小。哈哈)。值得注意的是:一般情况oracle是在块级维护ORA_ROWSCN,也就是说多个行共享一个ORA_ROWSCN号,这样很明显是不行的,你更新一行,其他的行都会因为这一行而把ORA_ROWSCN更新了,这样一来就会出现很多假更新警报

如何解决这个问题呢?我们必须用一个新的参数ROWDEPENDENCIES来重建这个数据段,把里面的数据块内的数据行每行都增加6个字节,使ORA_ROWSCN独立到行了。

不加就ROWDEPENDENCIES是  block-level 也就是这个row_scn是以块为基准的, 块上的任何一行修改了,这个块里面所有行的row_scn都会随之改变而 row-level dependency 则是说这里面的每一行的row_scn独立了, 块里面一样修改后,不影响这个块里面其他行的row_scn


默认表中修改块中的记录会影响本块其他记录的 ora_rowscn
如果加上ROWDEPENDENCIES ,那么修改记录只影响本记录的ora_rowscn,不会引起其他记录的改变
加上这个功能,会影响存储,每条记录会增加6字节的物理存储空间。看下面的例子: