一、业务对象或锁对象是多例的情况下
原因:业务中一般使用的lock对象锁,lock锁的范围是针对同一个对象里面不同的线程,也就是说,jvm锁是对象锁,对象之间锁不共用
有兴趣了解更深的也可以看一下lock锁的大致执行流程:
解决方案:保证业务对象和锁对象是单例,例如利用单例设计模式,spring的ioc容器对象管理......
可能有的小伙伴就会想到,既然上面两种导致锁失效的原因都是因为对象之间没有共用一把锁,那是不是用类锁就能解决这个问题?没错,如果使用sychronized中的类锁确实能解决这个问题,可又产生了另一个问题,也就是公平性的问题,sychronized是非公平锁,线程之间抢占资源顺序是随机,没有先到先得的规则,是允许插队的。一般业务上是不允许的,举个例子,在秒杀的业务场景下,一般比的是谁的手速和网速快,但在非公平锁的环境下,有可能后面点的慢或网速慢的人抢到了该商品,那是不是对于那些狂练手速和疯狂蹭网的很不公平?(补一句:lock锁没有类锁)
能保证对象的单例确实能解决这一个问题,但是下面这两种情况产生的问题才是问题的核心。
二、在使用了spring事务注解的情况下(不单是jvm锁,大部分锁实现都会出现这个问题)
原因:spring事务是基于aop的方式实现的,是包裹着整个方法的(包括锁),事务不在锁的范围内,很容易出现并发执行的时候,a方法的事务还没提交上去,b事务就读了数据库的旧值。
举个例子:
代理类中的方法实际代码是这样的 :
a事务还没提交,b事务就读取了数据库的旧值(流程图):
解决方案:
第一种: 用显示事务,将事务放在锁范围里面
第二种: 事务隔离级别改为读未提交(不推荐)
第三种: 再套一层方法,在外层方法使用锁
第四种: 最优雅的解决方法,将锁封装成注解的形式,并把优先级设置成比事务注解低
三、在服务集群的情况下
原因:服务都不一样了,锁和对象自然也不一样(就和第一个情况下的环境一样)
解决方法:利用mysql的排他锁机制,将所有业务sql集中成一条sql(以上三种问题都能解决,但是不灵活,只能在业务允许的情况下使用)
代码:
public void decrStock(String productCode) { UpdateWrapper<Stock> wrapper = new UpdateWrapper<>(); wrapper.eq("code", "a"). gt("quantity", 0). setSql("quantity = quantity - 1"); int updateStatus = stockMapper.update(new Stock(), wrapper); if (updateStatus == 0) { throw new StockException("当前库存不足"); } }
总结
综上所述,我们可以发现jvm锁只适合在单体项目中并且业务需求简单的情况下使用,所以有条件还是使用分布式锁吧。