基于Redisson实现分布式锁并分析其原理

目录

场景:

模拟测试:

redis依赖:

代码:

问题:没加锁,高并发场景下会出现超卖问题。比如当前有三个线程同时访问这段代码,同时判断当前库存大于0,都减一,然后保存,结果三个线程都减一然后保存,但是stock的值还是199

解决:

比如下面的场景:

下面进行高并发测试:

问题:

解决:给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,因此其他线程都能访问此段代码,导致超卖情况依然会发生。

解决:

最终解决:使用redisson框架。

引入依赖:

配置代码:

引入依赖

业务代码: 三行代码搞定

流程图:

原理:假设当前有两个线程:线程1 和 线程2 ,这两个线程同时访问业务代码。 假设线程1拿到锁了,那么它就会设置setnx命令,线程2 判断自己没拿到锁,它就会一直等待线程一释放锁然后进行加锁。线程一拿到锁后会在后台启动一个线程用于判断当前key是否存在,若存在则把key的存活时间额外加上当前key设置的存活时间的1/3 。 也就是说如果当前key的存货时间是9s,则在9s的基础上再加上3s 变成 12s。


分布式锁主要是为了解决高并发场景下的数据一致性问题的。一般就是涉及到多线程资源争抢时通过加锁来保证数据的安全性

场景:

模拟测试:

首先模拟一个抢购场景:

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帮我们做的加锁的处理。

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值