5

企业实战之分布式锁方案一步步的演变历程!

 3 years ago
source link: https://www.cnblogs.com/rxhh/p/14685342.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

企业实战之分布式锁方案一步步的演变历程!

在我们学习多线程开发的时候,在线程同时针对同一个资源进行操作的时候都需要加锁;一般会用到reentrantLock和synchronized两种锁方案,至于他们之间的区别也是面试的时候经常问到的,小伙伴们可自行网补。这里介绍企业经常用到的另一种锁,分布式锁。大家肯定听说过,但是就不一定用对哦。今天就深入的介绍一下分布式锁方案的演变

我们也不免俗套来举个并发扣除库存的例子

图片

我们来看一下代码

//扣除商品库存
//产品id: productId
//扣除数量: count
public void reduce(int productId,int count){
     //步骤1 从数据库获得产品实体
        Product product = getProduct(productId);
     //步骤2 获得当前库存数量
    int stockCount = product.getStock();
    if(stockCount >= count){
          //步骤3 扣除库存
          product.setStock(stockCount - count);
          //步骤4 把产品实体更新到数据库
          productService.update(product);
          log.info("购买成功!")
     }else{
          log.info("库存不足,无法购买!")  
     }
}

当前产品的库存数为10

请求A买了2个产品,那应该扣除2

请求B买了3个产品,那应该再扣除3

那最终的库存剩余为5

上面代码在分布式环境中,只要稍微流量大点,这边就会出现扣减库存不是预期的情况。原因就是

图片

两个请求同时到来时,都同时执行了步骤1,在同一时刻都获取到了同一个产品库存当前库存都为10;但在步骤3的时候都是用10减count值,那么不管是请求A和请求B哪个先执行步骤4,库存剩余要么剩余是8或者7;都不是最终的5。

原因知道了,那怎么解决?小伙伴想到的就是弄个锁,而且还要分布式锁。

分布式锁登场

上面的问题很多小伙伴应该都知道要用分布式锁,那用什么技术方案呢?我相信很多小伙伴都会说用redis方案,很简单setnx就行了

setnx命令 是redis的一条原生命令大意为 set if not exists, 在指定的key不存在的情况下,命令执行成功,如果key存在就命令执行不成功。

这个方案是很多公司都这么用的,那我们调整一下代码

图片

需要考虑到一些业务异常,需要把锁释放掉,加上try/finally,这个千万不要忘了

当是还是有一些问题,就是如果加锁成功后,业务没有完成。突然断电或者运维人员用kill -9命令把线程删除了;那就导致了锁一直没有释放,因为不会执行finally里面的代码了。

那怎么办呢?有经验的小伙伴应该就知道解决方案了

优化分布式锁

方案还是比较简单的,加个过期时间就行了

图片

这样即使断电,过了10秒钟之后锁也会自动过期,也就是失效;别的请求就可以正常请求了

现在到了这里,就是很多公司应用分布式锁的常用方案了。小伙伴们这样就没有问题了吗

我们来看看问题出现在哪里?我们来调整一下业务代码

图片

因为我们扣库存的业务,不可能像写的很简单的业务;正式场景中业务是比较多的,不可能就这么简单;如果业务代码执行的时间超出了锁的过期时间,那么锁到期失效了,但业务代码还没有执行完;这种场景就会导致数据错乱。

那这个问题怎么解决呢?

这个问题的本质是锁在没有执行完成业务时,到期失效了;那我们可以不让他失效不就行了吗?那怎么不让他失效呢?

方案很简单

启动一个后台线程,可以每3秒或者5秒执行一次,找到这个锁的key,延长这个锁key的过期时间;这样就达到了锁过期时间续期的功能了。是不是很简单?

我们自己写代码去实现是没有问题的,但是现在市面上已经有了轮子了,不需要我们自己再去写这个代码了,直接用人家的轮子;这个就是大名鼎鼎的Redisson

分布式锁Redisson

redisson是针对redis分布锁的强大的工具包,他提供了自动续期的功能,以及重入锁的功能,只需要引入

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.13.2</version>
</dependency> 

然后实例化

@Bean
publicRedisson redisson(){
    Config config = new Config();
config.useSingleServer().setAddress("redis://xxx:6379").setPassword("*
").setDatabase(0);
    return (Redisson) Redisson.create(config);
}

图片

用法非常简单。我们接下来分析他的原理以及源码,看看他是怎么续期的,和实现可重入的

我们先来看看他的加锁流程图

图片

1、线程去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。

2、线程去获取锁,获取失败: 一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。即会阻塞线程

redisson是执行的lua脚本的核心代码tryLockInnerAsync如下

图片

redis执行lua脚本是原子性的,要么都成功,要么都失败

下面的代码就是获取锁失败,就自旋;一直尝试获取锁

图片

续期的业务就是上面流程图中看门狗做的,这里需要注意的是,如果要让看门狗有效果,就不要设置过期时间,如果设置了过期时间看门狗就不起作用了;看源码图片

续期的代码就是this.scheduleExpirationRenewal(threadId);

注意判断条件,是if (leaseTime == -1L)的时候才生效,才会启动一个续期任务线程

小伙伴看到这里,是不是觉得这样实现分布锁应该就没有问题了吧,那是不是这样呢?

我们来看一个问题

图片

上图中我们发现,如果突然出现redis的主节点突然挂掉了,而且在设置锁key的时候,还没有来得及同步到Slave从节点中,

主节点挂了,从节点会顶上成为主节点;但是这个时候新的主节点是没有这个锁key的。

那问题就来了,其他的请求再起请求的锁,就会获取锁;这样就造成了业务的混乱。

当然这个情况还是蛮特殊的,蛮少见的。那这个问题怎么去解决呢?

我们来分析一下上面的问题,主要就是因为主从同步数据有延迟导致的。

我们先来了解一下分布式系统中的CAP理论

C表示数据一致性,A表示高可用性,P表示分区容错性

CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

那我们redis架构是AP原则,就是保证高可用性,牺牲了数据一致性;即主从数据有一定的延迟。

我们在来看看市面上还有另一种分布式锁的方案,就是zookeeper

zookeeper的架构原则是CP,就是保证了数据一致性,牺牲了高可用性,zookeeper的原理就是在写入数据时候,要保证主节点写入成功,而且还要保证大多数follow从节点也要写入成功,都写入成功,才会返回客户端成功。这样就保证了数据没有延迟。

不过小伙伴们应该能够发现,zookeeper的写入性能是稍微低点的,因为要保证主从都要写入成功才行。

在整个主流方案中,如果一定要保证数据一致性,保证锁不会有问题,可以选择zk方案实现分布式锁;但可以接受特殊情况下的锁错误,那就用redis的redisson方案(推荐)。可以根据不通业务去选择,即在同一个平台中,可实现不同的方案。

看完三件事❤️


如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 阿风的架构笔记 』,不定期分享原创知识。
  3. 同时可以期待后续文章ing🚀
  4. 关注后回复【666】扫码即可获取架构进阶学习资料包

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK