分布式锁主要是为了解决高并发场景下的数据一致性问题的。一般就是涉及到多线程资源争抢时通过加锁来保证数据的安全性
场景:
模拟测试:
首先模拟一个抢购场景:
redis依赖:
代码:
在redis数据库存了一个stock值,相当于是库存,value是200
逻辑:首先去从redis里面取库存,判断当前库存是否大于0,如果大于0则库存减一
问题:没加锁,高并发场景下会出现超卖问题。比如当前有三个线程同时访问这段代码,同时判断当前库存大于0,都减一,然后保存,结果三个线程都减一然后保存,但是stock的值还是199
解决:
乍一看是没问题了,但是想一下,我们的服务有可能要部署多台,那么synchronized就没用了。因为synchronized是仅限于当前服务所在的jvm虚拟机,也就是单机环境下才有效。另外一个相同的服务启动的时候在另一个jvm里面,synchronized是锁不住的。
比如下面的场景:
一个服务我部署了两台,前端通过nginx转发到不同的服务去
nginx:
当我访问根目录,它会转发到redislock 下的两个服务中去。好,现在启动两个服务
下面进行高并发测试:
工具:Jmeter
首先配置一个线程组:
然后在线程组下面配置你要并发压测的接口
然后配置接口:
配置请求次数及时间
在执行前还可以打开压测报告:
开始
选择压测计划保存位置,此处我选择保存到桌面
执行完毕:
查看后台日志:
可以看到:两台服务在扣减库存后日志有显示库存相同的数字。这说明当前synchroinzed失效了。一样出现了两台机器扣除库存,库存同时减一的超卖情况。
来到正题了。像这种分布式环境下的问题应该怎么处理呢?
答案就是:使用分布式锁 (使用redis、zookeeper)均可以实现
使用redis实现命令: setnx
这条命令和set命令的区别:
setnx 就是说如果当前setnx 的key 不存在,就会为key设置value值。否则不做任何操作。set命令就是为key设置value,如果key不存在则为此key设置value,如果存在则会用新的value覆盖旧的value。
代码实现:
执行完后删除key
我讲一下执行逻辑:
假设当前有三个线程通过nginx进来转发到两个服务里面,假设线程一先执行到这段代码,那么它就会判断redis里面key="lockey"存不存在,一看,不存在那么它就会setnx,为key等于lockKey设置value "zhuge"
那执行完肯定返回true啊,接着就走下面的逻辑。其他两个线程一看,key为"lockKey" 已经存在了呀,那么就直接返回false了,就不会执行下面的扣减库存的代码了。
问题:
那么第一个线程拿到锁了,当它要执行下面的代码的时候突然因为你的业务代码抛异常了,那么会出现什么后果?
肯定是这个key就删除不了了嘛,它删除不了那redis里面一直有这个key,其他线程又得不到这个锁,那岂不是一件商品都卖不出去了嘛
那怎么办呢?
有小伙伴会说:搞一个try catch块,把代码放进去,delete key 放到finally里面去不就完了嘛
这样是可以解决代码异常的问题,但是不是从个本上解决的。还有问题。试想一下,你有个线程加锁进来了,正在执行代码的时候宕机了,或者是运维执行了 kill -9暴力终止程序进程,那怎么办?那你这个key不还是执行不到嘛,数据库里面还是会有这个key。
解决:给key设置超时时间,过了这个时间自动删除 此处为10秒自动删除
问题:上面这两条设置key和超时时间的代码无法保证原子性,假如刚设置完key,服务挂了,怎么办?
解决:可以一条命令设置key和超时时间一步搞定
看下代码:
还有没有问题呢? 答案是有的。
问题:当前代码设置key时间为10s,但是执行业务逻辑的时候业务逻辑执行了15秒,由于key存在时间是10s ,肯定已经不存在了,当前环境有是高并发环境,另外一个线程进来一看,key不存在了,那么它就设置了一个key,设置存活时间为10s, 等到第二个线程又执行5s的时候,第一个线程代码已经走完了,由于finally里面会删除key,因此第一个线程会把key再次删除,此时它删除的是第二个线程刚刚设置的key,此时第三个线程进来了,一看key不存在,就设置key,存活时间依旧是10s,此时第二个线程走完删除key,把第三个线程的key删除...如此循环往复,会发现这个key一直就是被其他线程认为是不存在的key,因此其他线程都能访问此段代码,导致超卖情况依然会发生。
怎么办?
解决:
我们可以在线程进来的时候为key设置一个唯一的value,然后执行finanny的时候去判断当前value和线程进来的时候设置的value是否一致。若一致则判断当前线程是设置此value的线程,则可以删除key,否则就认定当前线程不是设置key的线程。
那么现在还剩下一个问题:
就是我设置的这个key过期时间如果小于业务代码执行的时间,依然会导致key被删除从而引起其它线程进来,依然会导致超卖问题的发生。那该怎么处理这个问题呢?
有同学会说:给它设置时间长一点不就完了嘛。那服务宕机了怎么办呢?如果我服务宕机了又重启了,你给key设置60s,那我最起码得等一分钟才能重新设置这个key
解决:其实可以设置一个后台定时任务每隔一段时间判断这个key是否存在。若存在则给它的时间延长一段时间。
最终解决:使用redisson框架。
引入依赖:
配置代码:
使用:
引入依赖
业务代码: 三行代码搞定
附上一段简单的代码:
public class TestController {
@Autowired
private static Redisson redisson;
public static void main(String[] args) {
String lockKey = "product_01";
// 获取锁对象
RLock lock = redisson.getLock(lockKey);
// 加锁
lock.lock();
// 释放锁
lock.unlock();
}
}
流程图:
原理:假设当前有两个线程:线程1 和 线程2 ,这两个线程同时访问业务代码。 假设线程1拿到锁了,那么它就会设置setnx命令,线程2 判断自己没拿到锁,它就会一直等待线程一释放锁然后进行加锁。线程一拿到锁后会在后台启动一个线程用于判断当前key是否存在,若存在则把key的存活时间额外加上当前key设置的存活时间的1/3 。 也就是说如果当前key的存货时间是9s,则在9s的基础上再加上3s 变成 12s。
redisson底层原理:
这其实就是一个lua脚本。上面那一段代码意思就是判断当前代码是否存在,如果不存在则设置key 和 value 然后 再设置超时时间 ARGV [1] 默认 30s
其实这么写跟上面说的这段代码很像
但是为什么redisson要这么写呢?没有原子性问题吗?
其实redis会保证这个lua脚本的原子性,他会把这个lua脚本当成一行代码去执行,要么全部成功,要么全部失败。
时间监听:
点进去就会发现 有个timeTask() 任务
这个脚本的意思是判断当前key是否存在 若存在则进行延时
如果执行成功再次调用此方法
默认时间:
以上就是redisson帮我们做的加锁的处理。