乐观锁和悲观锁的区别

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。


乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。


两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

 

1、无论是选择悲观锁策略,还是乐观锁策略。如果一个对象被上了锁,那么该对象都会受这个锁的控制和影响。如果这个锁是个排它锁,那么其它会话都不能修改它。

 

2、选择悲观锁策略,还是乐观锁策略,这主要是由应用和业务需求来确定的。如果你的应用和业务经常会出现从我看到要修改的记录的值,到我修改完成该记录这 个时间段内,该记录有较大概率被其它会话所修改。换句话说就是,在我真正去做出修改时,这个记录的值很可能已经与我当初看到的不同了。那么这时,采取悲观 锁策略,也许是更好的。而采取悲观锁策略的一个典型操作就是 select ... for undate。通过这种操作,使得从我一开始查看该记录起,这条记录就被上了排它锁,不允许其它会话再对该记录有任何修改。

 

相对的,如果你的业务和应用基本上很少出现这种情景,那么选择乐观锁策略也许就会更好。

 

3、这两种策略的核心其实就是持有锁的时间的起止点不同,悲观锁是从读取记录的那一刻就开始了,而乐观锁只从UPDATE那一刻开始;结束的点两者是一样 的,都是发出commit或rollback命令。所以,悲观锁策略会使锁的持续时间更长,而乐观锁的持续时间则较短。其影响就是并发。悲观锁的并发性低 于乐观锁。

 

4、无论是采用哪种策略,都要保证数据的完整性。所以,在采用乐观锁策略时,是有可能出现数据的不完整。举例来说:储户甲的存款余额100元,假设在几乎 相同的时刻,发生了两笔业务,业务1为其存入了50元,另一个业务是其网上购物消费了30元。显然,这两个操作结束后,甲的存款余额应为120元 (100+50-30)。但我们设想一下在数据库层面,可能出现这种情况,当其在银行柜台存入50元时,银行操作员收到了甲存入的50元现金,并通过 select 语句看到甲的当前余额为100元(其发出的指令是下面的语句:

 

select 余额

 

   from 存款余额表

 

where 储户帐号=储户甲的银行帐号;)

 

,接着,发出指令

 

update 存款余额表

 

      set 余额=150

 

    where 储户帐号=储户甲的银行帐号;

 

但就在其看到甲的余额为100元,到其修改甲的余额为150这期间,甲在网上的消费行为导致交易系统已经将甲的余额变成了70元(100-30)并提交 了。当银行员工发出的指令也被提交后,甲的余额变成了150元,相当于甲网上消费的行为没有产生任何扣款。这显然是不正确的,更是要避免出现的。

 

如果这套系统采用的是悲观锁策略,那么在从银行员工查看甲当前余额的那一个时刻起(这时查询的指令就会是:

 

select 余额

 

   from 存款余额表

 

where 储户帐号=储户甲的银行帐号 for update;)

 

该记录就已经被锁定了,这时甲网上消费的行为导致的交易系统从甲的帐户中扣减的操作就会处于等待状态。直至银行员工提交了相关指令,交易系统才能去扣减甲的钱款。这样,就可以确保甲的帐户余额是正确的。

 

悲观锁的策略显然可以保证业务的正确性和完整性。但再设想一下,如果甲在存款时,银行员工内急,或者储户甲说等一等,我要考虑一下是否再多存一些。那么, 银行员工的操作就不会提交,这时网上交易系统对甲帐户的扣款操作就会一直处于等待状态,或者在等待一定时间后,返回一个扣款失败的提示。这对于系统的效率 和客户来说,都不是一个好的体验。

 

5、因为考虑到悲观锁策略可以产生的这种问题,所以,我们在设计应用时,可以采取一些其它方法来避免上述情况的发生。其思想就是在真正提交时,确认要修改的数据没有变化过。主要的方法如下:

 

(1)、更新时带入原始的数据。

 

     update 存款余额表

 

      set 余额=150

 

    where 储户帐号=储户甲的银行帐号 and 余额=100;

 

(2)、在记录上增加修改的时间戳(也可利用ora_rowscn伪列)。即在事务开始时,获取该记录的时间戳,修改时,校验该时间戳,若一致则修改。

 

6、其实,我上面举的这个例子,如果在业务设计时,选择更新指令为

 

update 存款余额表

 

      set 余额=余额+50

 

    where 储户帐号=储户甲的银行帐号;

 

那么,即使是在乐观锁策略的情况下,依然可以保证数据的正确性和完整性。

 

posted on 2016-07-20 23:09  金麟  阅读(564)  评论(0编辑  收藏  举报

导航