欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

从0到1认识Redis分布式锁

程序员文章站 2022-03-23 23:28:55
今天来说说Redis分布式锁。在说Redis分布式锁之前你首先得明白什么是分布式。在我看来服务部署就两种形式,一种是单体应用,一种是分布式架构。那么什么叫单体应用呢? 举个简单的例子,比如你的网段ip是 192.168.xxx.xxx,你只有一个服务,就部署在这一台ip上,那么我认为这种就是单体应用。那么什么又叫分布式架构? 你可以这样理解,比如你的应用最开始上市平平无奇,没有什么访问量,那么单体应用看起来并不会什么问题,完全够用嘛。假如说某一天,你的app火了,成千上万甚至几十万的用户访问到你的....

今天来说说Redis分布式锁。
在说Redis分布式锁之前你首先得明白什么是分布式。
在我看来服务部署就两种形式,一种是单体应用,一种是分布式架构。
那么什么叫单体应用呢? 举个简单的例子,比如你的网段ip是 192.168.xxx.xxx,你只有一个服务,就部署在这一台ip上,那么我认为这种就是单体应用。
那么什么又叫分布式架构? 你可以这样理解,比如你的应用最开始上市平平无奇,没有什么访问量,那么单体应用看起来并不会什么问题,完全够用嘛。假如说某一天,你的app火了,成千上万甚至几十万的用户访问到你的系统,那么所有的用户请求全部用一台服务器去承载这是会有大问题的。会造成系统拥堵,cpu过高,系统响应慢,频繁fgc,甚至一台机器可能扛不住,直接挂了。那你的整个系统就瘫痪了。带给用户的体验就是一卡一卡的,然后突然就宕机了。
那么这时候完蛋了,一个大好的赚钱机会就被你单体架构部署给弄丢了。
所以这个时候,我们往往会去采取分布式架构部署服务,在所有的应用前通过一层服务(服务你可以理解为网关或者nginx等等一系列可以分发请求的服务)去分发流量,给系统一个良好并且稳定的工作环境,增加系统的可用性以及稳定性。

在跟你聊什么是分布式锁之前,我们先来看看这样的一种场景。
假如你有一个商品服务,你把他部署了分布式架构,部署了两台,然后通过nginx来分发流量。
然后商品服务里面有一个接口是获取库存接口,伪代码如下

@RestController
public class MarketController {

    @Resource
    private MarketService marketService;

    public String getMarket(String userId){
        //获取库存  默认100个
        int num = marketService.getMarketNum();
        System.out.println("当前库存:" + num + ",用户userId拿到了第" + num +"号商品");
        num--;
        //重新修改库存
        marketService.setMarketNum(num);

        return "购买成功!";
    }
}

假如对这段操作我们没有任何的上锁操作,那么就会出现超卖的问题。
什么是超卖?
就比如张三和李四同时在抢商品,当前商品是85,当张三拿到了85号商品,系统还没有进行减库存的操作时,李四这时候也进入了这个接口,因为数据库还没被更改,那么李四也拿到了85号商品,这就出现了超卖现象。同一件商品被卖给了两位顾客,你觉得合适吗? 顾客可能心里美滋滋,但是对于系统而言就是资损了。我们肯定不能允许这种问题存在。

好,那么这个时候你已经意识到了要上锁来防止问题的出现了。

那要用什么锁呢? 可能有同学会想到,哎用那个synchronized去锁啊,锁住这一整个方法不就可以了吗?
比如这样

@RestController
public class MarketController {

    @Resource
    private MarketService marketService;

    public String getMarket(String userId){
        synchronized (this) {

            //获取库存  默认100个
            int num = marketService.getMarketNum();

            System.out.println("当前库存:" + num + ",用户userId拿到了第" + num + "号商品");
            num--;

            //重新修改库存
            marketService.setMarketNum(num);

        }
        return "购买成功!";
    }
}

如果你是单体架构,那么我认为这堂课我们已经可以下课了

开个玩笑,开个玩笑 ->_ ->
其实还没有结束,你是单体应用的话,确实这样锁住防止了并发,但是其实这把锁是一把悲观锁,效率不高。
而且,如果你是分布式架构,这样就有问题,你只能锁住当前这台虚拟机部署的应用的这个库存方法,当两台服务同时出现用户进行库存操作的时候,其实还是会出现超卖的问题。归根结底,就是多把锁不同步,如果我们能够用一把锁去控制,那这样是不是就不会出现并发的问题了?比如这样

@RestController
public class MarketController {

    @Resource
    private MarketService marketService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public String getMarket(String userId){

           //设置分布式锁
            Boolean ifAbsent = stringRedisTemplate.opsForValue()
            .setIfAbsent("userId", "product-01", 30, TimeUnit.SECONDS);
            if(!ifAbsent){
                return "当前系统正在处理,请稍后再试!";
            }
            //获取库存  默认100个
            int num = marketService.getMarketNum();

            System.out.println("当前库存:" + num + ",用户userId拿到了第" + num + "号商品");
            num--;

            //重新修改库存
            marketService.setMarketNum(num);

            //移除分布式锁
            stringRedisTemplate.delete("userId");

            return "购买成功!";
    }

}

这种样子会不会有问题呢?
其实是有的,假如服务在设置完redis锁之后,到修改库存的那一步,突然程序发生了异常。会发生什么?
程序会回滚,但是redis的操作已经把这把锁给设置上去了。那这把分布式锁就成了一把死锁了,就有大问题了,同学。

好,又有一个同学说,那可以try-catch起来,用finally去删除 不就好了吗,不管发生啥异常,我最后都会要删除掉这把分布式锁,就像这样

@RestController
public class MarketController {

    @Resource
    private MarketService marketService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public String getMarket(String userId){

        try {
            //设置分布式锁
            Boolean ifAbsent = stringRedisTemplate.opsForValue()
            .setIfAbsent("userId", "product-01", 30, TimeUnit.SECONDS);
            if(!ifAbsent){
                return "当前系统正在处理,请稍后再试!";
            }

            //获取库存  默认100个
            int num = marketService.getMarketNum();

            System.out.println("当前库存:" + num + ",用户userId拿到了第" + num + "号商品");
            num--;

            //重新修改库存
            marketService.setMarketNum(num);

        } catch (Exception e) {
           //异常处理
        } finally {
            //移除分布式锁
            stringRedisTemplate.delete("userId");
        }

        return "购买成功!";
    }

}

我们再来看看这样的结构会不会有问题,
try-catch确实可以解决所有异常,甚至是错误,但是当设置完redis锁之后,到修改库存的那一步,你机器突然不争气的宕机了呢?
那么死锁的问题还是出现了。
怎么解决?

可能有同学就说了,设置过期时间啊,过期时间搞一个不就完事了吗? 你再怎么宕机异常,我这把锁我总归不会成为死锁,就像这样

@RestController
public class MarketController {

    @Resource
    private MarketService marketService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public String getMarket(String userId){

        try {
            //设置分布式锁
            Boolean ifAbsent = stringRedisTemplate.opsForValue()
            .setIfAbsent("userId", "product-01", 30, TimeUnit.SECONDS);
            if(!ifAbsent){
                return "当前系统正在处理,请稍后再试!";
            }

            //获取库存  默认100个
            int num = marketService.getMarketNum();

            System.out.println("当前库存:" + num + ",用户userId拿到了第" + num + "号商品");
            num--;

            //重新修改库存
            marketService.setMarketNum(num);

        } catch (Exception e) {
           //异常处理
        } finally {
            //移除分布式锁
            stringRedisTemplate.delete("userId");
        }

        return "购买成功!";
    }

}

现在我们再来仔细看看这个程序,不论你宕机还是异常这把锁都会自动过期,不会死锁,程序正常的时候也能去锁住这个接口,防止并发问题导致的超卖现象。看上去好像是没什么问题了。
你再仔细看看,真的没问题了吗?
再仔细看看
再看看

我来举个栗子

从0到1认识Redis分布式锁

如果我们设置的redis超时时间是30秒。
第一个线程在执行修改库存时,这个时候数据库可能卡了,导致线程1整个时间超过了30秒,这个时候还没能够执行完,因为这个时候,分布式锁已经失效了,这个时候第二个线程就可以进来了,那么这个时候,超卖问题是不是又发生了?显然是发生了。
这个时候,可能又有同学会说,时间拉长啊,再拉个几倍呢?
其实不然,不论你时间怎么调其实都会有问题
如果你调小,那有可能就是我上面描述的这种问题,锁过期了但是第一个线程还没能够把程序执行完毕,其他线程就可以进来,这样就会有问题
如果你调大,那有可能发生异常,你重启服务,这个功能要等个好几十秒甚至几分钟才能用,这样也不友好。所以总的来说,不能通过依赖这个过期时间去解决这个问题
虽然这个过期时间其实是可以解决百分之九十九以上的问题的。并且如果你的系统对这个功能点的并发要求不是那么高,其实做到这样子就差不多了。

如果你对一致性有很高的要求,也不想有这种因为时间而导致的并发问题,其实还有一种解决方案
想象一下,如果有另外一个线程,可以在主线程执行这段代码的过程中不断的去看这把锁有没有过期,如果锁快要过期了,就给锁延长一下存在的时间,其实说白了也就是锁续命
通过这种续命的方式,在你的主线程执行完之前都保留有这把分布式锁,这样就可以防止过期时间导致的锁过期的问题了。

那么如何去做呢? 其实市面上已经有比较成熟的框架了。
比如说 Redisson
那么Redisson是怎么去实现这一过程的呢? 我们一起根据源码看一看

本文地址:https://blog.csdn.net/qq_42865087/article/details/110531761

相关标签: redis