前言
在我们学习多线程开发的时候,在线程同时针对同一个资源进行操作的时候都需要加锁;一般会用到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("库存不足,无法购买!")
}
}
购买场景
上面代码在分布式环境中,只要稍微流量大点,这边就会出现扣减库存不是预期的情况。原因就是
两个请求同时到来时,都同时执行了步骤1,在同一时刻都获取到了同一个产品库存当前库存都为10;但在步骤3的时候都是用10减count值,那么不管是请求A和请求B哪个先执行步骤4,库存剩余要么剩余是8或者7;都不是最终的5。
原因知道了,那怎么解决?小伙伴想到的就是弄个锁,而且还要分布式锁。
分布式锁登场
上面的问题很多小伙伴应该都知道要用分布式锁,那用什么技术方案呢?我相信很多小伙伴都会说用redis方案,很简单setnx就行了。
这个方案是很多公司都这么用的,那我们调整一下代码
当是还是有一些问题,就是如果加锁成功后,业务没有完成。突然断电或者运维人员用kill -9命令把线程删除了;那就导致了锁一直没有释放,因为不会执行finally里面的代码了。
那怎么办呢?有经验的小伙伴应该就知道解决方案了
优化分布式锁
方案还是比较简单的,加个过期时间就行了
这样即使断电,过了10秒钟之后锁也会自动过期,也就是失效;别的请求就可以正常请求了
问题分析
我们来看看问题出现在哪里?我们来调整一下业务代码
因为我们扣库存的业务,不可能像写的很简单的业务;正式场景中业务是比较多的,不可能就这么简单;如果业务代码执行的时间超出了锁的过期时间,那么锁到期失效了,但业务代码还没有执行完;这种场景就会导致数据错乱。
那这个问题怎么解决呢?
解决思路
这个问题的本质是锁在没有执行完成业务时,到期失效了;那我们可以不让他失效不就行了吗?那怎么不让他失效呢?
方案很简单
我们自己写代码去实现是没有问题的,但是现在市面上已经有了轮子了,不需要我们自己再去写这个代码了,直接用人家的轮子;这个就是大名鼎鼎的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);
}
修改代码
用法非常简单。我们接下来分析他的原理以及源码,看看他是怎么续期的,和实现可重入的
源码分析
我们先来看看他的加锁流程图
加锁机制
redisson是执行的lua脚本的核心代码tryLockInnerAsync如下
下面的代码就是获取锁失败,就自旋;一直尝试获取锁
续期机制
续期的业务就是上面流程图中看门狗做的,这里需要注意的是,如果要让看门狗有效果,就不要设置过期时间,如果设置了过期时间看门狗就不起作用了;看源码
续期的代码就是this.scheduleExpirationRenewal(threadId);
注意判断条件,是if (leaseTime == -1L)的时候才生效,才会启动一个续期任务线程
主从问题
小伙伴看到这里,是不是觉得这样实现分布锁应该就没有问题了吧,那是不是这样呢?
我们来看一个问题
上图中我们发现,如果突然出现redis的主节点突然挂掉了,而且在设置锁key的时候,还没有来得及同步到Slave从节点中,
那问题就来了,其他的请求再起请求的锁,就会获取锁;这样就造成了业务的混乱。
当然这个情况还是蛮特殊的,蛮少见的。那这个问题怎么去解决呢?
解决方案
我们来分析一下上面的问题,主要就是因为主从同步数据有延迟导致的。
我们先来了解一下分布式系统中的CAP理论
那我们redis架构是AP原则,就是保证高可用性,牺牲了数据一致性;即主从数据有一定的延迟。
我们在来看看市面上还有另一种分布式锁的方案,就是zookeeper;
不过小伙伴们应该能够发现,zookeeper的写入性能是稍微低点的,因为要保证主从都要写入成功才行。
总结
在整个主流方案中,如果一定要保证数据一致性,保证锁不会有问题,可以选择zk方案实现分布式锁;但可以接受特殊情况下的锁错误,那就用redis的redisson方案(推荐)。可以根据不通业务去选择,即在同一个平台中,可实现不同的方案。
看完三件事❤️
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
- 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
- 关注公众号 『 阿风的架构笔记 』,不定期分享原创知识。
- 同时可以期待后续文章ing🚀
- 关注后回复【666】扫码即可获取架构进阶学习资料包