首页 > 技术文章 > Redis学习-分布式锁

daijiting 2020-04-30 16:01 原文

  在高并发环境下,我们往往会对同一个数据进行操作,比如说商品的库存数据。如果有多个线程都要来对库存数据进行操作,那势必会造成线程的安全性问题,如何来解决这个问题就是我们今天要介绍的。

一、线程锁

  根据上面的问题,我们可以立刻想到一种解决方案,那就是在给线程加锁。

 

  上图是我们最常见的一种对线程加锁的方式,方式很简单只需要在对库存数据操作的过程中,加上锁就行,我们来看这段加锁的代码:

  

/**
     * 操作商品的数量
     *
     * @param goods 商品名称
     * @param number 要购买的数量
     * @return
     */
    public String findStock(String goods,Integer number) {
        //对同样的商品加锁
        synchronized (goods){
            //先找到原来的数量
            Integer shopNumber=shop.getNumber();
            //如果不够直接返回
            if (shopNumber< number){
                return "商品库存不够";
            }
            //如果够则扣除库存,并返回
            shop.setNumber(shopNumber-number);
        }
        return "商品购买成功";
    }

  可以看到上图是直接对同一商品进行了加锁,那么当第一个线程获取了锁,第二个就无法再次进入。

   这种模式在单机情况是可行的,但是在我们的分布式业务中,不同的项目是在不同的服务器上运行,即使同一服务器,也是在多个JVM上面运行。而这里提供的方式是线程锁,它的使用条件是在同一进程中使用,因此在分布式系统中,这种方式显然是行不通的,这就需要一种新的方法来解决-分布式锁。

二、分布式锁

  分布式锁是指:在应用场景是在分布式或集群环境下,多个服务(每个服务可以看做是一个进程)处理同一的数据,防止多进程处理数据时出现的进程间安全问题的锁。

  一般来说在java应用中,分布式锁的的解决方案就是redis

  

 

 

   可以看到,分布式锁的实现和线程锁的实现几乎是一致的,最大的区别在于,进程获取锁失败后,不能像线程那样当锁释放后能够及时进行操作。

  分布式锁的使用逻辑:

 /**
     * 分布式事务锁的设计
     * @param goods  商品名称
     * @param number 要购买的数量
     * @return
     */
    public  String findStockByRedis(String goods,Integer number) throws InterruptedException {
        //如果锁获取失败,那证明其他服务正在使用
        while (!lock(goods)) {
            //获取失败时,我们直接等待一段时间
            //每5秒一次,再去获取锁
            TimeUnit.SECONDS.sleep(5);
        }
        //获取成功后,操作商品的数量
       String str= findStock(goods,number);
        //操作完成后,记得解锁
        unlock(goods);
        return str;
    }

   我们重点来看两个锁的方法:

  在Redis中,实现锁的方法就是setnx命令 ,这个命令的作用是:命令在指定的 key 不存在时,为 key 设置指定的值。

  因此如果当一个客户端设置了key后,其他客户端就不能在去生成,这样就能够实现一个锁。

  lock方法:

  

/**
     * 获取锁
     * @param goods
     * @return
     */
    public Boolean lock(String goods){
        Boolean lockStatic=  redisTemplate.opsForValue().setIfAbsent(goods,"lock",30,TimeUnit.SECONDS);
        if (lockStatic == null) {
            lockStatic=false;
        }
        return  lockStatic;
    }

    可以看到我们添加了一个商品的key,如果有一个用户在操作这个商品,其他用户来操作都是失败的。

  其次还必须给key加上过期时间,这样做主要是防止该服务意外宕机时,不会一直占用锁。

  unlock解锁方法:

  解锁方法非常简单,只需要删除这个key就可以实现解锁功能。

public Boolean unlock(String goods){
        return redisTemplate.delete(goods);
    }

分布式锁的总结:

  上述的方法其实还有许多问题:

  第一点,在进程中如果获取锁失败后,会一直尝试获取锁,这就类似于一个自旋锁的状态。我们知道自旋锁都是非常损耗性能的,因此那一部分还能进行优化处理。

  第二点,其实我们讨论的都是在单机redis情况下的解决方案,如果是在redis集群环境下的话,这个方案显然是不行的。解决redis集群环境下的分布式锁,可以使用redisson框架。

 

推荐阅读