Oracle中的数据锁定机制

    技术2022-05-11  25

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

            数据的锁定分为两种方法,第一种叫做悲观锁,第二种叫做乐观锁。什么叫悲观锁呢,悲观锁顾名思义,就是对数据的冲突采取一种悲观的态度,也就是说假设数据肯定会冲突,所以在数据开始读取的时候就把数据锁定住。而乐观锁就是认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让用户返回错误的信息,让用户决定如何去做。

            先从悲观锁开始说。在SqlServer2000等其余很多数据库中,数据的锁定通常采用页级锁甚至是表级锁的方式,也就是说对一张表内的数据操作采取的是一种串行化的更新插入机制,在任何时间同一张表只会插入或者更新删除1条数据,别的想插入的数据要等到这一条数据插完以后才能依次插入。带来的后果就是性能的降低,在多用户并发访问的时候,当对一张表进行频繁操作时,会发现响应效率很低,很多操作处于一种等待状态,数据库经常处于一种假死状态。而Oracle用的是行级锁,只是对想锁定的数据才进行锁定,其余的数据不相干,所以在Oracle表中并发插数据的时候,基本上不会有任何影响。顺路再说一下,许多非Oracle数据库对于数据库锁的管理采用链表的形式,也就是说要查找一个数据是否锁定,需要在一个专门的已锁定数据管理链表中进行遍历查询,如果查找到,那么就会表示数据已锁定,如果没有查找到,那么就表示数据没有被锁定可以操作。如果这个时候被锁定的数据量不断上升,遍历链表的时间也逐渐增加,最后会导致数据库性能急剧下降。而Oracle采取的是Latch的形式进行数据锁定,也就是在数据块的有一个标示,表示这条数据当前被锁定。如果要确定数据是否被锁定,只要检查当前数据块的Latch标示快就可以了,即使被锁定的数据量增加,性能也不会因此而下降。

            Oracle的悲观锁需要利用一条现有的连接,从SQL语句的区别来看,分成两种方式,一种是select  for update语句锁定,一种是select 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

     

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

    select   *   from  test  where  id  =   10   for   update

     

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

    select   *   from  test  where  id  =   10

     

    也就是没有for update这种锁定数据的语句的话,就不会造成阻塞了。也就是说,数据的锁定是一种共享锁,对普通的Select语句不产生影响。另外一种情况,就是当数据库数据被锁定的时候,也就是执行刚才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照样不能对数据进行锁定更新等等。也就是说update以及delete操作对数据的锁定形式和select for update是同样的效果

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

     

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

            第一种就是在数据取得的时候把整个数据都copy到应用中,在进行提交的时候比对当前数据库中的数据和开始的时候更新前取得的数据。当发现两个数据一模一样以后,就表示没有冲突可以提交,否则则是并发冲突,需要去用业务逻辑进行解决。这种方法可以不用对数据库的物理结构进行改变,也就是不需要使用DDL语句对数据库表进行变更,不需要添加额外的列来标示版本,不过由于把所有的数据库列都进行比较,工程比较浩大,在实际应用中由于表的列可能众多,比较不切实际。

            第二种乐观锁的做法就是采用版本戳,这个在Hibernate中得到了使用。采用版本戳的话,首先需要在你想进行乐观锁的数据库表上建立一个新的column,比如为number型,初始化为1,当你数据每更新一次的时候,版本数就会往上增加1。比如同样有2session同样对某条数据进行操作。两者都取到当前的数据的版本号为1,当第一个session进行数据更新后,在提交的时候查看到当前数据的版本还为1,和自己一开始取到的版本相同。就正式提交,然后把版本号增加1,这个时候当前数据的版本为2。当第二个session也更新了数据提交的时候,发现数据库中版本为2,和一开始这个session取到的版本号不一致,就知道别人更新过此条数据,这个时候再进行业务处理,比如对整个第二个Transaction都进行Rollback等等操作。在用版本戳的时候,可以在应用程序侧使用版本戳的验证,也可以在数据库侧采用Trigger(触发器)来进行验证。不过数据库的Trigger的性能开销还是比较的大,而且如果Trigger编写不当,容易造成数据库的死锁,所以能在应用程序侧进行验证的话还是推荐不用Trigger的方式

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

            第四种做法就是采用散列算法。这种做法和第一种做法有点类似,就是把当前数据内容进行传递,最后在提交的时候进行比较。但是区别还是比较大的,第一种做法把整个更新前的数据都copy了下来进行传递,而这种做法只是把数据做了一个散列码,所以传递的数据要小的多,网络负荷也会因此小很多。这种采用散列算法的做法如果在Oracle侧生成散列的话,有三种方法可以生成。第一种就是利用Oracle8i 8.1.5以后的OWA_OPT_LOCK.CHECKSUM函数来进行数据的散列生成比如下面的例子

    select  owa_opt_lock.checksum (to_char(sysdate, ' YYYYMMDDHHMISS ' ) )  from  dual 

     在这里我用这个OWA_OPT_LOCK.CHECKSUM函数对当前的系统时间进行了散列,用这个方法生成的出土的可能性是65536,因为生成的散列算法是一个2进制16位的值,所以值范围也就是65536个值而已。冲突的概率不会太大,也就1/65536,可以近似认为不可能。

    第二种散列算法采用Oracle8i 8.1.7以后提供的DBMS_OBFUSCATION_TOOLKIT.MD5方法,不过这个方法不能直接在SQL语句里面使用,需要在存储过程中调用。所以可以先写一个共通的存储过程的Function,然后利用这个function去进行加密。

    第三种散列方法就是用Oracle 10g Release1种提供的DBMS_CRYPTO.HASH方法。这个方法可以计算一个SHA-1或者是MD5摘要,所以如果实际情况下使用Oracle 10g的数据库,建议采用这个方法。不过和第二种类似,这个方法也是需要采用存储过程才能够编写,不能直接应用在SQL语句中的。所以可以预先在数据库中编写一个函数,调用DBMS_CRYPTO.HASH方法,然后应用在数据库的散列生成中。

            最后一种做法就是采用Oracle 10g所带有的ORA_ROWSCN函数来进行乐观锁。ORA_ROWSCN是根据系统最后更新时间来进行计算。这个ORA_ROWSCN在默认情况下是采用数据块为单位的,也就是一个数据库块(block)上共享一个ORA_ROWSCN,当数据更新的时候,这个block快的ORA_ROWSCN就会自动更新。所以在默认情况下的话,有可能出现假冲突的情况。比如ABCD四条数据都在一个block上,这个时候A数据更新了,ORA_ROWSCN也会更新,这个时候因为ABCD四条数据存储在一个block上,所以BCD的ORA_ROWSCN也更新过了,其实BCD三条数据并没有更新过,这个就造成了假更新的情况出现。见下面的SQL语句  

    select  id, name, location, value, ora_rowscn  from  test

    检索结果如下

    IDNAMELOCATIONVALUEORA_ROWSCN10ACCOUNTINGNEW YORK161079520RESEARCHDALLAS161079530SALESCHICAGO161079540OPERATIONSBOSTON1610795

     然后执行如下的更新语句

    update  test  set  value = 2   where  id  =   20

    在数据commit过后,会发现ORA_ROWSCN发生了变化,如下所示

    IDNAMELOCATIONVALUEORA_ROWSCN10ACCOUNTINGNEW YORK161115120RESEARCHDALLAS261115130SALESCHICAGO161115140OPERATIONSBOSTON1611151

            对于这种情况还是可以有办法解决的,就是利用Oracle 9i提供的ROWDEPENDENCIES建表关键字,这个关键字在Oracle9i中是为了增加行依赖性跟踪特性的,支持推进复制。在Oracle10g中有可以用来做行级别的ORA_ROWSCN用了。用这个关键字建表以后,每一行的ORA_ROWSCN就会独立了。在每行会增加一个隐藏的COLUMN,所以每行会增加6个byte的开销。建表语句如下所示:

      CREATE   TABLE  TEST (ID, NAME, LOCATION, VALUE,  CONSTRAINT  test_pk  PRIMARY   KEY (ID)) ROWDEPENDENCIES  AS   SELECT  deptno, dname, loc,  1   FROM  scott.dept 同样再执行 update  test  set  value = 2   where  id  =   20

    语句后,我们再看一下commit过后检索的结果

    IDNAMELOCATIONVALUEORA_ROWSCN10ACCOUNTINGNEW YORK161137020RESEARCHDALLAS261141030SALESCHICAGO161137040OPERATIONSBOSTON1611370

    可以看到这次只有id=20的数据的ORA_ROWSCN发生了更新。所以这个时候我们就可以利用ORA_ROWSCN作为乐观锁,用不着专门再增加一个额外的域了。而且这个域是Oracle负责维护的,开销也不是很大,不过需要Oracle 10g才可以使用。如果是Oralce10g的数据库应用的话,还是比较推荐用这种办法。

        总之,介绍了Oracle的锁定机制,究竟是悲观锁好还是乐观锁好,其实也是不一定的。Oracle中悲观锁还是很不错的,而且从开始的时候就把数据锁定。免除了后面的很多冲突处理。不过悲观锁需要保持一个Oracle连接,在我们常见的B/S应用中,特别是数据先取得,然后让用户再更新,再返回提交这种流程来说,悲观锁是不大可能的。首先是因为B/S应用中,一般是利用一个连接池,在两次Http Request请求都是不同的数据库Connection。而且也不能锁定一个数据太长时间,否则人人都这么锁定,应用很容易进入死锁状态,这个时候就要采用乐观锁了。而在乐观锁中,虽然采用散列的效果不错,网络传输数据会减少很多,而且不用在数据库中增加COLUMN,不过由于散列计算比较占用CPU,所以还是需要仔细考虑一下的。ORA_ROWSCN可以说是比较好的选择,不过第一需要重新建表,第二需要在Oracle10g下才有这个特性,所以也不是到处都可以使用,当一个万金油的。

     


    最新回复(0)