高并发场景下锁的使用技巧

本文来源:html

https://www.cnblogs.com/jackyfei/p/12142840.htmlredis

如何确保一个方法,或者一块代码在高并发状况下,同一时间只能被一个线程执行,单体应用可使用并发处理相关的 API 进行控制,但单体应用架构演变为分布式微服务架构后,跨进程的实例部署,显然就没办法经过应用层锁的机制来控制并发了。那么锁都有哪些类型,为何要使用锁,锁的使用场景有哪些?今天咱们来聊一聊高并发场景下锁的使用技巧。算法

锁类别

  不一样的应用场景对锁的要求各不相同,咱们先来看下锁都有哪些类别,这些锁之间有什么区别。数据库

  • 悲观锁(synchronize)
  • Java 中的重量级锁 synchronize
  • 数据库行锁
  • 乐观锁
  • Java 中的轻量级锁 volatile 和 CAS
  • 数据库版本号
  • 分布式锁(Redis锁)

乐观锁

  就比如说是你是一个生活态度乐观积极向上的人,老是往最好的状况去想,好比你每次去获取共享数据的时候会认为别人不会修改,因此不会上锁,可是在更新的时候你会判断这期间有没有人去更新这个数据。架构

  乐观锁使用在前,判断在后。咱们看下伪代码:并发

1reduce()
2{
3    select total_amount from table_1
4    if(total_amount < amount ){
5          return failed.  
6    }  
7    //其余业务逻辑
8    update total_amount = total_amount - amount where total_amount > amount; 
9}复制代码

  • 数据库的版本号属于乐观锁;
  • 经过CAS算法实现的类属于乐观锁。

悲观锁

  悲观锁是怎么理解呢?相对乐观锁恰好反过来,老是假设最坏的状况,假设你每次拿数据的时候会被其余人修改,因此你在每次共享数据的时候会对他加一把锁,等你使用完了再释放锁,再给别人使用数据。框架

  悲观锁判断在前,使用在后。咱们也看下伪代码:分布式

1reduce()
2{
3    //其余业务逻辑
4    int num = update total_amount = total_amount - amount where total_amount > amount; 
5   if(num ==1 ){
6          //业务逻辑.  
7    } 
8}复制代码

  • Java中的的synchronize是重量级锁 ,属于悲观锁;
  • 数据库行锁属于悲观锁;

扣减操做案例

  这里举一个很是常见的例子,在高并发状况下余额扣减,或者相似商品库存扣减,也能够是资金帐户的余额扣减。扣减操做会发生什么问题呢?很容易能够看到,可能会发生的问题是扣减致使的超卖,也就是扣减成了负数。微服务

  举个例子,好比个人库存数据只有100个。并发状况下第1笔请求卖出100个,第2批卖出100元,致使当前的库存数量为负数。遇到这种场景应该如何破解呢?这里列举四种方案。高并发

方案1:同步排它锁

  这时候很容易想到最简单的方案:同步排它锁(synchronize)。可是排他锁的缺点很明显:

  • 其中一个缺点是,线程串行致使的性能问题,性能消耗比较大。
  • 另外一个缺点是没法解决分布式部署状况下跨进程问题;

方案2:数据库行锁

  第二咱们可能会想到,那用数据库行锁来锁住这条数据,这种方案相比排它锁解决了跨进程的问题,可是依然有缺点。

  • 其中一个缺点就是性能问题,在数据库层面会一直阻塞,直到事务提交,这里也是串行执行;
  • 第二个须要注意设置事务的隔离级别是Read Committed,不然并发状况下,另外的事务没法看到提交的数据,依然会致使超卖问题;
  • 缺点三是容易打满数据库链接,若是事务中有第三方接口交互(存在超时的可能性),会致使这个事务的链接一直阻塞,打满数据库链接。
  • 最后一个缺点,容易产生交叉死锁,若是多个业务的加锁控制很差,就会发生AB两条记录的交叉死锁。

方案3:redis分布式锁

  前面的方案本质上是把数据库看成分布式锁来使用,因此一样的道理,redis,zookeeper都至关于数据库的一种锁,其实当遇到加锁问题,代码自己不管是synchronize或者各类lock使用起来都比较复杂,因此思路是把代码处理一致性的问难题交给一个可以帮助你处理一致性的问题的专业组件,好比数据库,好比redis,好比zookeeper等。

  这里咱们分析下分布式锁的优缺点:

  • 优势:
  • 能够避免大量对数据库排他锁的征用,提升系统的响应能力
  • 缺点:
  • 设置锁和设置超时时间的原子性;
  • 不设置超时时间的缺点;
  • 服务宕机或线程阻塞超时的状况;
  • 超时时间设置不合理的状况;

加锁和过时设置的原子性

  redis加锁的命令setnx,设置锁的过时时间是expire,解锁的命令是del,可是2.6.12以前的版本中,加锁和设置锁过时命令是两个操做,不具有原子性。若是setnx设置完key-value以后,尚未来得及使用expire来设置过时时间,当前线程挂掉了或者线程阻塞,会致使当前线程设置的key一直有效,后续的线程没法正常使用setnx获取锁,致使死锁。

  针对这个问题,redis2.6.12以上的版本增长了可选的参数,能够在加锁的同时设置key的过时时间,保证了加锁和过时操做原子性的。

  可是,即便解决了原子性的问题,业务上一样会遇到一些极端的问题,好比分布式环境下,A获取到了锁以后,由于线程A的业务代码耗时过长,致使锁的超时时间,锁自动失效。后续线程B就意外的持有了锁,以后线程A再次恢复执行,直接用del命令释放锁,这样就错误的将线程B一样Key的锁误删除了。代码耗时过长仍是比较常见的场景,假如你的代码中有外部通信接口调用,就容易产生这样的场景。

设置合理的时长

  刚才讲到的线程超时阻塞的状况,那么若是不设置时长呢,固然也不行,若是线程持有锁的过程当中忽然服务宕机了,这样锁就永远没法失效了。一样的也存在锁超时时间设置是否合理的问题,若是设置所持有时间过长会影响性能,若是设置时间太短,有可能业务阻塞没有处理完成,是否能够合理的设置锁的时间?

续命锁

  这是一个很不容易解决的问题,不过有一个办法能解决这个问题,那就是续命锁,咱们能够先给锁设置一个超时时间,而后启动一个守护线程,让守护线程在一段时间以后从新去设置这个锁的超时时间,续命锁的实现过程就是写一个守护线程,而后去判断对象锁的状况,快失效的时候,再次进行从新加锁,可是必定要判断锁的对象是同一个,不能乱续。

  一样,主线程业务执行完了,守护线程也须要销毁,避免资源浪费,使用续命锁的方案相对比较而言更复杂,因此若是业务比较简单,能够根据经验类比,合理的设置锁的超时时间就行。

方案4:数据库乐观锁

  数据库乐观锁加锁的一个原则就是尽可能想办法减小锁的范围。锁的范围越大,性能越差,数据库的锁就是把锁的范围减少到了最小。咱们看下面的伪代码

1reduce()
2{
3    select total_amount from table_1
4    if(total_amount < amount ){
5          return failed.  
6    }  
7    //其余业务逻辑
8    update total_amount = total_amount - amount;  
9}复制代码

  咱们能够看到修改前的代码是没有where条件的。修改后,再加where条件判断:总库存大于将被扣减的库存。

1update total_amount = total_amount - amount where total_amount > amount复制代码

  若是更新条数返回0,说明在执行过程当中被其余线程抢先执行扣减,而且避免了扣减为负数。

  可是这种方案还会涉及一个问题,若是在以前的update代码中,以及其余的业务逻辑中还有一些其余的数据库写操做的话,那这部分数据如何回滚呢?

  个人建议是这样的,你能够选择下面这两种写法:

  • 利用事务回滚写法:

  咱们先给业务方法增长事务,方法在扣减库存影响条数为零的时候扔出一个异常,这样对他以前的业务代码也会回滚。

1reduce()
 2{
 3    select total_amount from table_1
 4    if(total_amount < amount ){
 5          return failed.  
 6    }  
 7    //其余业务逻辑
 8    int num = update total_amount = total_amount - amount where total_amount > amount; 
 9  if(num==0) throw Exception;
10}复制代码

  • 第二种写法
1reduce()
 2{
 3    //其余业务逻辑
 4    int num = update total_amount = total_amount - amount where total_amount > amount; 
 5   if(num ==1 ){
 6          //业务逻辑.  
 7    }  else{
 8    throw Exception;
 9  }
10}复制代码

  首先执行update业务逻辑,若是执行成功了再去执行逻辑操做,这种方案是我相对比较建议的方案。在并发状况下对共享资源扣减操做可使用这种方法,可是这里须要引出一个问题,好比说万一其余业务逻辑中的业务,由于特殊缘由失败了该怎么办呢?好比说在扣减过程当中服务OOM了怎么办?

  我只能说这些很是极端的状况,好比忽然宕机中间数据都丢了,这种极少数的状况下只能人工介入,若是全部的极端状况都考虑到,也不现实。咱们讨论的重点是并发状况下,共享资源的操做如何加锁的问题。

总结

  最后我来给你总结一下,若是你能够很是熟练的解决这类问题,第一时间确定想到的是:数据库版本号解决方案或者分布式锁的解决方案;可是若是你是一个初学者,相信你必定会第一时间考虑到Java中提供的同步锁或者数据库行锁。

  今天讨论的目的就是但愿把这几种场景中的锁放到一个具体的场景中,逐步去对比和分析,让你可以更加全面体系的了解使用锁这个问题的前因后果。我是张飞洪,但愿个人分享能够帮助到你。

img

线程同步手记

漫谈什么时候从单体架构迁移到微服务?

微服务的时间和成本去哪儿了

微服务学习导航

为何在作微服务设计的时候须要DDD?

假如你是架构师,你要作些什么

微服务划分的姿式

Java IO模型之NIO模型

MongoDB 集群构建:分片+副本+选举

Fork-Join框架

Spring Boot实现动态增删启停定时任务

MongoDB - 用户与权限

SpringForAll社区,2019年文章精选10篇

img

本文由博客一文多发平台 OpenWrite 发布!

相关文章
相关标签/搜索