SpringCloud系列 - 分布式锁(八)
目录
一、案例引入
1.1 模拟并发安全问题
1.2 解决办法:加锁!
二、锁失效情况
1.1 错误使用本地锁
1.2 集群模式下本地锁失效
1.3 单实例微服务中本地锁的风险
三、解决办法
3.1 调整sql语句
3.2 分布式锁
四、基于Redis的分布式锁
4.1 介绍
4.2 Redisson
4.2.1 导入依赖
4.2.2 创建配置类
4.2.3 lock锁
(1)思考
(2)小结
4.2.4 lock锁 - 公平锁
4.2.5 lock锁 - 读写锁
(1)演示
(2)小结
4.2.6 信号量
4.2.7 闭锁
五、疑问解答
5.1 单体应用需要使用分布式锁吗
5.2 为什么实际工作代码中很多写操作都不加锁
5.3 有了分布式锁,还需要引入数据库锁吗?
5.4 有了分布式锁还需要本地锁吗
一、案例引入
1.1 模拟并发安全问题
创建一张数据库表,库存表,我们暂且只记录一条记录。
调用/stock/reduce接口,模拟实现减库存
减库存业务逻辑如下:
首先查询数据库中指定商品在目标仓库的当前库存量。
当确认该商品库存存在且数量大于零时,再执行库存扣减操作。
启动项目,我们通过调用该接口,模拟试一试!
我们连续点击了10次,按理说数据库减少库存10件,还剩4990,我们查看一下
没有问题。可见在没有并发调用时,接口一切正常。
但是并发情况下呢?
接下来先把数据库的剩余库存改回5000,我们通过jmeter模拟一下并发:
开启100个线程,每个线程执行50次,按理说,就可以把5000库存清零。
通过jmeter的聚合报告,发现5000次全部执行完成,未出现异常
我们看看数据库的变化
未清零,还剩4487,出现并发安全问题。
这是为什么呢?
每个请求都可视为独立的线程。当这些线程并发执行时,由于库存修改操作需要时间处理,在某个特定时刻,多个查询线程可能会获取到相同的库存数量值,从而导致数据一致性问题。
1.2 解决办法:加锁!
将数据库数据还原5000,重新模拟并发:
可以看到,我们加上锁以后,吞吐量变小了。但是是否就解决了并发安全问题呢?看看库存量是否清零就行了。
还剩2488,并没有清零。不是说加锁就可以解决吗?
其实啊,原因在这里。
我们加上了@Transactional事务注解,上面这段代码的执行顺序变成了
开启事务 -> 获取锁 -> 执行业务代码 -> 解锁 -> 事务提交
事务默认是在方法执行前开启,方法结束后提交。然而我们的锁加在了这个方法的内部,导致当前事务尚未提交时就释放了锁,其他线程趁机进入了该方法执行操作,从而引发数据安全问题。
所以不加事务是不会有这个问题的,但通常情况下,我们是会选择使用@Transactional注解的,保证事务一致性。所以还有一个方法:可以修改一下执行顺序。例如:
执行效率上明显更慢了,但是并发安全问题得到了保障!
二、锁失效情况
1.1 错误使用本地锁
像前面的案例,我们一开始把锁加在事务方法内部,导致事务还未提交就解锁了,引发了并发安全问题。
1.2 集群模式下本地锁失效
当我们某个应用部署了多个实例,搭建集群,这种情况下本地锁也会导致失效。
多实例下并发竞争问题更明显,前面单体单实例部署的解决方案用普通锁,但在多实例下是行不通的。因为普通锁不能跨进程生效。
我们举个例子演示一遍就知道了
使用Nginx进行负载均衡。
这里我们就使用window版的,这样方便些,启动前修改配置文件nginx.conf
然后启动Nginx,打开jmeter进行压测。记得数据库库存改回去。
可以看到集群模式,我们的吞吐量上来了,但是并发安全问题还是没有解决,也就是本地锁失效!
1.3 分布式(微服务)架构琐
所以说,集群模式下也好,分布式架构下包括微服务也好,并发情况下,本地锁都会有并发安全问题。
那么微服务系统每个服务只有一台机器呢?,一旦牵扯到服务之间的调用,只要