高并发下锁的使用技巧

1、我们为什么要使用锁?
2、有哪些锁可以使用?

Java中的重量级锁Synchronize,Java中的轻量级锁和CAS,数据库行锁,悲观锁,乐观锁,分布式锁

3、这些锁之间有什么区别么?

我们也一起来探讨下,理清楚这些概念后,我们会通过一个非常具备说明性的例子,库存扣减或余额扣减的场景,来分别讨论前面我们提到的那些锁,它们是如何使用的,又有哪些缺点呢?

了解这些之前呢,我们要先要来看看为什么要使用锁,如何确保一个方法,或者一块代码在高并发情况下,同一时间只能被一个线程执行。

单体应用可以使用Java并发处理的相关API进行控制,但是,单体应用架构演变为分布式微服务架构后,跨JVM或者跨进程的实例部署,就没有办法通过Java的锁机制来控制并发了,在这种情况下,为了解决跨JVM并发访问的问题,就需要一种跨JVM的互斥机制,来控制共享资源的访问,这时候,就需要引入分布式锁,刚才讲到演变场景中出现的锁,可以总结为几类:
Java重量级锁Synchronize,Java轻量级锁volatile,CAS算法等,数据库行锁,Redis锁,ZooKeeper,先进先出的分布式队列等,数据库版本号乐观锁。

接下来我们一起看一下这些锁,哪些是乐观锁,哪些是悲观锁,先理解一下什么是乐观锁,什么是悲观锁,乐观锁就好比说是你是一个生活态度乐观积极向上的人总是往最好的情况去想,比如您每次去获取共享数据的时候会认为别人不会修改,所以不会上锁,但是在更新的时候你会判断这期间有没有人去更新这个数据。
有这么两种方法可以去判断,一种是数据库的版本号机制,一种是CAS算法实现,悲观锁是怎么理解呢,相比乐观锁而言悲观锁是反过来的,总是假设最坏的情况,假设你每次拿数据的时候会被其他人修改,所以你在每次拿共享数据的时候会对它加一把锁,等你使用完了,释放了锁,再给别人使用数据。因此,我们可以看出Java synechronize的是重量级锁,也是悲观锁,Java中通过CAS的思想来实现的类都是乐观锁的机制,比如java.util.concurrent.atomic类,数据库行锁属于悲观锁,数据库版本号属于乐观锁,volatile相比synchronize的是一种轻量级的同步机制。

我们刚刚聊过了为什么要使用锁,有哪些锁,以及这些锁有哪些区别,接下来更进一步讲,这里我将举一个非常常见的例子,
在高并发情况下余额扣减,或者类似商品库存扣减的例子,也可以是资金账户的余额扣减。我们一起来看一下,如果是扣减操作,会发生什么问题呢,很容易可以看到,可能会发生的问题是扣减导致的超卖,也就是扣减成了负数。举个例子,比如我的库存数据有100个,并发情况下第一笔请求卖出100个,第二笔请求卖出,100个,但是总库存只有100个,就会发生第二笔在获取库存的时候发现有100个,可是当时扣减的时候,已经被第一个请求扣减掉了,导致当前的库存扣减为负数,这时候很容易想到最简单的方案,同步排它锁synchronize,但是排它锁的缺点很明显,其中一个缺点是,线程串行导致的性能问题,性能消耗比较大,另一个缺点是无法解决分布式部署情况下跨进程、跨JVM问题。

进而我们再深入想一下,可能会想到,那用数据库行锁select for update,来锁住这条数据,这种方案相比synchronize排它锁,解决了跨进程的问题,但是依然有缺点。我们一起看一下,其中一个缺点就是性能问题。在数据库层面,select for update会一直阻塞直到事务提交,这里也是串行执行。再来看第二个缺点,需要注意设置事务的隔离级别是Read Committed,否则并发情况下,另外的事务法看到其他的数据,依然会导致超卖问题。缺点三是什么呢,其实就在于容易打满数据库连接。现象是,如果不小心在这个事务注解的方法类,除了有数据库操作,还有第三方的接口交互动作的话,由于第三方接口交互的网络连接超时的可能性,会导致事务连接一直被阻塞,打满数据库连接。来看看最后一个缺点,容易产生交叉死锁,如果多个业务的加锁顺序控制不好,就会发生AB两条记录的交叉死锁,首先事务1和事务2同时分别取得了记录1和记录2的排它锁,然后事务1又取得了记录2的排它锁,这时候等待事务2释放记录2的排它锁,但是事务2释放记录2的前期是,必须事务1释放记录1的排它锁,这样1和2两条记录就会因为相互锁等待产生死锁。

再来看一看方案三,使用redis分布式锁,前面的方案二本质上是把数据库当做分布式锁来使用,所以同样的道理,Redis、ZooKeeper等都相当于数据库的一种锁,其实当遇到加锁问题,代码本身无论是synchronize,或者各种Lock,使用起来都比较复杂,所以思路是把代码处理的一致性的问题、难题,交给一个能够帮助你处理一致性的问题的专业组件,比如数据库,比如Redis,比如ZooKeeper等,这里我们来讨论一下,Redis做分布式锁这种方案的优缺点,引入Redis分布式锁,可以避免大量对数据库排它锁的争用,提高系统的响应能力,但是Redis分布式锁也有一些小的缺点,我们一起来看一看,Redis分布式锁的使用,以及应该注意的几个问题,设置锁和设置超时时间的原子性,不设置超时时间的缺点,服务宕机和线程阻塞超时的情况,超时时间设置不合理的情况,Redis加锁的命令是setnx,设置锁的过期的时间是expire,解锁的命令是del,但是Redis2.6.12之前的版本中,由于加锁命令和设置锁过期时间是两个操作,不具备原子性,如果setnx命令设置完key-value之后,还没有来得及使用expire命令来设置过期时间,当前线程挂掉了或者线程阻塞了,会导致当前线程设置的keyyi一直有效,后续的线程无法正常通过setnx获取锁,造成死锁。针对这个问题,Redis2.6.12以上的版本,为set命令增加了可选的参数,可以在加锁的同时,设置key的过期时间,保证了加锁和过期动作的原子性的,但是即使解决了redis加锁和过期动作的原子性问题,业务上同样会遇到一些极端的问题。比如分布式环境下,线程A获取到了锁,但是在获取到锁之后,因为线程A的业务代码耗时过长,导致锁的超时时间,锁自动失效,后续线程B就意外的持有了锁,之后线程A再次恢复执行,直接用del命令释放锁,这样就错误地将线程B,同样key的锁误删除了,代码耗时过长还是比较常见的场景,假如你的代码中有外部通用的接口调用,就容易产生这样的场景。刚才讲到的线程超时阻塞的情况,那么,如果不设置超时间呢,当然也不行,如果线程A在持有锁的过程中突然服务宕机了,这样锁就永远无法失效了,同样的,也存在锁超时时间设置是否合理的问题,如果设置锁持有时间过长,会影响性能,如果设置超时间过短,有可能业务阻塞没有处理完成,是否可以合理地设置所得超时间,这是一个很不容易解决的问题,不过有一个办法能解决这个问题,那就是续命锁,我们可以先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间之后重新去设置这个锁的超时时间,续命锁的实现过程就是写一个守护线程,然后去判断对象锁的情况,过一段时间,锁快失效的时候再次进行续命加锁,但是一定要判断锁的对象是否同一个,不能乱续,同样的,主线程业务执行完了,守护线程也需要销毁,避免资源浪费,
使用续命锁的方案相对比较而言,更复杂,所以如果业务比较简单,可以根据经验类比,合理的设置锁的超时间就行,最后再来看看方案四,数据库乐观锁。加锁的一个原则呢,就是尽量想办法减少锁的范围,锁的范围越大,性能越差,数据库乐观锁就是把所有的范围减少到了最小,把之前的代码中update修改成这样的形式,我们可以先看看修改之前的代码是没有where条件的,
修改后,增加where条件,判断总库存大于将被扣减的库存,如果更新条数返回0,说明在执行过程中被其他线程抢先执行了扣减,并且避免了扣减为负数,但是这种方案还会涉及一个问题,如果在update的之前的代码中,以及其他的业务逻辑中,还有一些其他的数据库写操作的话,那这部分数据如何回滚呢,我的建议是这样的,你可以选择下面这两种写法,我们先看利用事务回滚的写法,给业务方法增加事务,方法在扣减库存影响条数为0的时候,扔出一个异常,这样update之前的业务代码也会回滚,我们再来讨论第二个写法,首先执行update业务逻辑,如果update执行成功了,再去执行business logic其他数据库的操作,
这种方案是我相对比较建议的方案,在并发情况下对共享资源扣减操作,可以使用这种方案,但是这里需要引出一个问题,比如说万一其他业务逻辑中的业务因为特殊原因失败了该怎么办呢,比如说在扣减过程中服务OOM了怎么办,我只能说这些非常极端的情况,比如突然宕机中间数据都丢了,这种极少数的情况下,只能人工介入。如果所有的极端情况都考虑到,也不现实,我们讨论的重点是并发情况下,共享资源的操作如何加锁的问题。

今天的分享到这里就结束了,最后我来给你总结一下,如果你可以非常熟练地解决这类问题,第一时间肯定想到的是,数据库版本号解决方案或者分布式锁的解决方案,但是如果你是一个初学者,相信你一定会第一时间考虑到java中提供的同步锁,或者数据库行锁,今天探讨锁目的,就是希望把这几种场景的锁放到一个具体的场景中,逐步去对比和分析,
让你能够更加全面体系的了解使用锁这个问题的来龙去脉。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值