利用redis分布式锁实现秒杀业务

  • 什么是分布式锁 

为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分布式锁

  • 如何利用redis实现分布式锁

我们先看一段秒杀业务的逻辑代码:

 int stock = Integer.parseInt(template.opsForValue().get("stock"));
            if (stock > 0){
                stock -= 1;
                template.opsForValue().set("stock",stock+"");
                System.out.println("秒杀成功!" + stock);
            }else {
                System.out.println("秒杀失败!");
            }

简单分析一下这段代码的功能:

1.在redis中设置String类型的键值对,键为"stock",值为库存数量,均为字符串形式存储在redis中。

2.如果库存大于0,则秒杀成功,库存减一,否则秒杀失败。

再来分析一下这段代码中存在的问题:

多线程的情况下会出现线程安全问题,线程不同步导致超卖问题。

解决办法:给这段代码加锁,也就是加上synchronized关键字,同步代码块或者同步方法均可。

上面我们只考虑的是在单机模式下,下面我们来看一下分布式的情况下,假设有两台机器都部署了这段代码,架构图如下所所示:

 

我们可以看到,在分布式系统中,请求被转发到不同的服务器进行处理,虽然对于独立的server1和server2来说,加了同步处理之后都是线程安全的,但是对于整个系统来说并不是,也就是说进程server1和server2并不是同步的。

这时候就需要分布式锁来同步这两个线程了,我们看一下下面的代码,是对之前代码的补充:

           String Lock_Key = "lock";
        
            //1.所有线程进来先尝试获取锁,redis中的setnx命令
            Boolean id = template.opsForValue().setIfAbsent(Lock_Key, clientId);
            //未拿到锁则返回错误,加锁失败
            if (!id)
                return "error";

            //2.成功拿到锁之后执行秒杀的业务逻辑
            int stock = Integer.parseInt(template.opsForValue().get("stock"));
            if (stock > 0){
                stock -= 1;
                template.opsForValue().set("stock",stock+"");
                System.out.println("秒杀成功!" + stock);
            }else {
                System.out.println("秒杀失败!");
            }

        
            //3.释放锁
            template.delete(Lock_Key);

        

简单分析一下上面这段代码:

1.在执行秒杀逻辑之前,利用redis的setnx命令向redis中获取锁(如果设置成功代表成功拿到了锁,否则失败返回失败信息)

2.成功获取锁之后执行秒杀逻辑

3.秒杀完成后释放锁

其实,就是增加了redis这么一个第三方变量来控制两台服务器的同步,redis其实具有一个信息传递的作用,相当于控制中心,控制两台服务器的同步工作。

存在的问题:

如果在释放锁之前出现了异常导致锁无法被正常释放,则会产生死锁问题,也就是key为"stock"的键一直存在,后面的所有线程都无法拿到锁。

可以进一步优化如下:

public  String stock(){
        String Lock_Key = "lock";
        try {

            //1.所有线程进来先尝试获取锁,redis中的setnx命令
            Boolean id = template.opsForValue().setIfAbsent(Lock_Key, clientId);
            template.expire(Lock_Key,30L, TimeUnit.SECONDS);  //设置锁的过期时间
            //未拿到锁则返回错误,加锁失败
            if (!id)
                return "error";

            //2.成功拿到锁之后执行秒杀的业务逻辑
            int stock = Integer.parseInt(template.opsForValue().get("stock"));
            if (stock > 0){
                stock -= 1;
                template.opsForValue().set("stock",stock+"");
                System.out.println("秒杀成功!" + stock);
            }else {
                System.out.println("秒杀失败!");
            }

        }finally {
            //3.释放锁,无论秒杀是否成功都要释放锁
            template.delete(Lock_Key);
        }
        return "end";

优化到这一步,这段秒杀业务可以说是比较完善了,但是在高并发环境下还会有很多的问题。

存在的问题:

在高并发环境下,会发生上一个线程把下一个线程刚设置的锁给删除的情况,这是因为我们之前为了能够保证释放锁,给锁设置了超时时间。

1.假设线程一执行到第三步释放锁之前,这时锁过期了(达到了过期时间)

2.这时候线程二进来了,执行完获取锁的操作

3.然后线程一此时还没有结束,它会继续执行,但是对于线程一而言它的锁已经删除了,但是线程一还会执行删除锁的步骤,而这时候线程二刚刚拿到锁,就被线程一给删除了。

4.最终造成的结果就是分布式锁永久失效。

解决办法:

在最后一步释放锁时添加一个判断条件,判断该锁是不是自己加的,如果是则删除,不是则不删除。

代码如下:

public  String stock(){
            String Lock_Key = "lock";
            String clientId = UUID.randomUUID().toString();
            try {

                //1.所有线程进来先尝试获取锁,redis中的setnx命令
                Boolean id = template.opsForValue().setIfAbsent(Lock_Key, clientId);
                template.expire(Lock_Key,30L, TimeUnit.SECONDS);
                //未拿到锁则返回错误,加锁失败
                if (!id)
                    return "error";

                //2.成功拿到锁之后执行秒杀的业务逻辑
                int stock = Integer.parseInt(template.opsForValue().get("stock"));
                if (stock > 0){
                    stock -= 1;
                    template.opsForValue().set("stock",stock+"");
                    System.out.println("秒杀成功!" + stock);
                }else {
                    System.out.println("秒杀失败!");
                }

            }finally {
                //3.释放锁,无论秒杀是否成功都要释放锁
                if (template.opsForValue().get(Lock_Key).equals(clientId))
                    template.delete(Lock_Key);
            }
            return "end";

通过为每一个线程加锁时设置自己的UUID防止了锁永久失效的问题。

终极版(Redisson框架),在Redisson中已经为我们封装好了这样一套逻辑来解决锁永久失效的问题,Redisson实现分布式锁的原理如图所示:

 

从上图我们可以看到,Redisson的解决方案是定期检测当前线程时候持有锁,如果持有则会延长锁的过期时间,而其他线程则会一致循环尝试加锁,直到加锁成功,这样就解决了当前线程“意外”释放了其他线程的锁的问题。

所以最终的优化代码如下:

public  String stock(){
            String Lock_Key = "lock";
            //获取锁的实例
            RLock lock = redisson.getLock(Lock_Key);
            try {

                //1.所有线程进来先尝试获取锁,redis中的setnx命令
                Boolean id = template.opsForValue().setIfAbsent(Lock_Key, clientId);
                template.expire(Lock_Key,30L, TimeUnit.SECONDS);
                //未拿到锁则返回错误,加锁失败
                if (!id)
                    return "error";
                
                //Redisson中加锁逻辑
                lock.lock(30, TimeUnit.SECONDS);

                //2.成功拿到锁之后执行秒杀的业务逻辑
                int stock = Integer.parseInt(template.opsForValue().get("stock"));
                if (stock > 0){
                    stock -= 1;
                    template.opsForValue().set("stock",stock+"");
                    System.out.println("秒杀成功!" + stock);
                }else {
                    System.out.println("秒杀失败!");
                }

            }finally {
                //3.释放锁,无论秒杀是否成功都要释放锁
                lock.unlock();
            }
            return "end";
    }

如有不足,欢迎指正,感谢浏览!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值