Java -- 浅谈 “悲观锁” 和 “乐观锁” - Go语言中文社区

Java -- 浅谈 “悲观锁” 和 “乐观锁”


一、前言

 

        悲观锁乐观锁不是真正意义上的锁,说白了就是Java JDK中就没有这俩货的位置(没有具体的实现类);

 

        悲观锁乐观锁就是一种思想,说白了就是一种为了保证业务系统高并发下的数据修改安全性而提出来的一种解决方案;

 

        悲观锁认为:我此时操作的数据,万一别人也正在修改怎么办,不行,我得独占,我得给数据加把锁,等我执行提交完,或事务成功回滚后,我再把锁释放,给下一个人用;

        

        乐观锁则认为:这数据,这一刻,就我自己用,哪来的其他人?哈哈哈,我才不管这其中会不会有人也来操作数据,我只要最后提交数据的时候,判断一下数据是不是修改成功就行了;

        

        在Java锁的概念中,同步锁(synchronize)、独享锁(写锁)就是一种悲观锁,而实现了CAS(Comare And Swap)思想的锁就是一种乐观锁

 


 

二、悲观锁的实现方式

 

        悲观锁的实现方式是基于数据库层面上的,如给整张表加锁,如给表的某一行加锁(也就是要操作的数据); 不同数据库都会提供不同的锁机制;

        在对任意记录进行修改前,都会尝试为该记录加锁,如果加锁失败,说明记录正在被修改,那么当前查询、修改可能要等待或者抛出异常;如果加锁成功,那么就可以对记录做出相应的修改,等事务完成或者回滚成功后就会释放锁;

        因此悲观锁的实现,不需要我们在应用层面上写逻辑代码。

 


 

三、乐观锁的实现方式

 

        乐观锁的实现方式是基于应用层面上的,如通过给表增加一个字段:版本号,来标记当前数据操作的计数;如果数据在最后提交时,发现之前取到的Version号改变了,就回滚事务,则本次修改失败;

     

(1)  举个反列(没有采用任何锁实现并发操作):

           表A:id(主键,Long),name(商品名称,VarChar),stock(库存,Int)

           假如有一商品信息如下:

           

     

商品库存表M

 
id name stock
1 华为mate rs 100


 

 

 


 

现有两个用户,A和B,同一时间购买了该商品,会出现什么情况?

分析下,首先是查询:

select name,stock from M where id = 1

A和B看到的手机库存stock的值均等于100(毋庸置疑,对吧),接下来就是A和B同时触发update操作(减库存):

A用户对应的数据层面上的操作:update M set stock = 100 - 1 where id = 1  , 结果是  stock = 99

B用户对应的数据层面上的操作:update M set stock = 100 - 1 where id = 1  , 结果是  stock = 99

最后你会发现,用户A和B下单分别购买了两部华为mate rs手机,手机库存量本应该是98,但实际情况却是99!

这种情况发生后,是很要命的,假如是101个人同时购买的话,就会直接造成该商品的"超卖",如果用户量再大一些,商家会哭的

 


 

(2)  举个正列(采用乐观锁实现并发操作):

           表A:id(主键,Long),name(商品名称,VarChar),stock(库存,Int),version(版本号,Long)

           更改商品信息如下:

           

商品库存表M

id name stock version
1 华为mate rs 100 0

 

 

        


 

现有两个用户,A和B,同一时间购买了该商品,应该怎么处理,避免上述(1)中的库存超卖呢?

首先还是查,不过这次也要把版本号给带上:

select name,stock,version from M where id = 1

此时A和B拿到的stock和version值是一样的

stock = 100 , version = 0

 

这里博主要讲一下事务四大特性中的隔离性,而隔离性又分不同的级别,具体关于事务的介绍可以转到我的另一篇博文

Spring-Boot + Atomikos 实现跨库的分布式事务管理

 

MySql数据库默认的事务隔离级别是可重复读(REPEATABLE-READ

 

 

同一事物中,多次读取某个数据,结果是一样的

 


 

PostGreSql数据库默认的事务隔离级别是:

 

 

只有事务提交时,才能读到数据

 


 

然后就是A和B对应的业务层面上的减库存操作,update稍作改动:

update 表 set stock  = #{stock} - 1 , version = #{version} + 1 where id = #{id} and version = #{version}

A :  update M set stock = 100 -1 ,version = 0+1 where id = 1 and version = 0

A执行时,发现修改的version值和之前查出来的version值一样,都是0,于是乎,version值变为1,事务提交


 

B:update M set stock = 100 -1 ,version = 0+1 where id = 1 and version = 0

B执行时,发现要修改的version值和之前查出来的version值居然不一样(此时version = 1,而B拿到的还是之前查到的0),怎么办,update当然是无效的,返回0行受影响

 

 

        程序中,一旦我们发现update的结果是0,我们就知道,我们要修改的这条数据是别人已经修改过的了,为了保证数据的可靠性、安全性和原子性,我们只能放弃本次修改,如抛异常触发事务回滚或手动回滚当前事务;

    

       我们借用网上一张图,再来呈现一下上述减库存的整个过程:

 

 

       这种情况在冲突不是太多的业务系统中(也就是流量较小,并发量不是太高的),相比悲观锁来说,性能是很OK的,因为数据的操作不涉及锁的占有和释放,反之,则用户体验效果会很糟糕,因为总是会提示用户"xxxx下单失败,请稍后再试...";

       


 

四、总结

 

  根据两种锁的特点,我们总结一下什么时候应该用悲观锁,什么时候应该用乐观锁?

 

(1)追求业务系统响应速度的    ,请选择乐观锁;要么事务提交失败,要么成功!不存在获取和释放锁操作;

(2)追求业务系统响应成功率的,请选择悲观锁;数据在进行操作之前,需要先拿到锁,确保了事物提交的结果都是正确的;

(3)如果选择了乐观锁,就要承受高并发下数据操作出现的频繁冲突!解决办法,提交失败的时候,再查一次数据,然后继续更新,直到修改成功!但是频繁的查询会严重影响性能,因此乐观锁适合读操作多的业务场景。

(4)如果选择了悲观锁,就要承受高并发下数据操作的性能卡顿发生!但是确保了写入数据的正确率,因此悲观锁适合写操作多的业务场景。

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/Appleyk/article/details/94730942
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-02-13 12:02:52
  • 阅读 ( 1404 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢