秒杀系统下的分布式锁redis实现

在springboot的日益成熟之下,分布式架构越来越普遍,面临的问题也随之增加,分布式锁就是其中之一,以前我们都是使用synchronized来处理并发请求,虽然也支持分布式,但是总有一下业务不适合,我们首先来看一个例子:秒杀系统

synchronized关键字

public synchronized void sellProduct(String productId){
      	//1、查询该商品库存,为0则活动结束
        int stockNum = stock.get(productId);
        if (stockNum == 0){
            throw new SellException(100,"活动结束");
        }else {
            //2、下单(模拟不同用户id不同)
            orders.put(KeyUtil.getUniqueKey(),productId);
            //3、减库存
            stockNum = stockNum -1 ;
            try{
                Thread.sleep(100);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            stock.put(productId,stockNum);
        }
    }

现在有A、B、C三种商品在参加秒杀活动,这个时候如果有一个A商品在调用sellProduct方法,那么B和C商品就都需要等待A商品调用这个方法之后释放锁才能执行,同一时刻只能完成一件商品的减库存操作,这样就造成了系统的性能瓶颈,也不符合秒杀系统的设计思想。由于 synchronized 无法做到细粒度的控制,从而引进了分布式锁,分布式锁能够完成 synchronized 无法做到的点。

redis实现分布式锁

分布式锁可以使用redis或者其他技术来完成,这里我们记录一下使用redis实现分布式锁。

引入redis依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

使用

public void sellProduct(String productId){
  	 //加锁
     long time = System.currentTimeMillis()+TIMEOUT;
     if (!redisLock.lock(productId,String.valueOf(time))){
         throw new SellException(1000,"人太多啦,再试试吧!");
     }
     
   	//1、查询该商品库存,为0则活动结束
     int stockNum = stock.get(productId);
     if (stockNum == 0){
         throw new SellException(100,"活动结束");
     }else {
         //2、下单(模拟不同用户id不同)
         orders.put(KeyUtil.getUniqueKey(),productId);
         //3、减库存
         stockNum = stockNum -1 ;
         try{
             Thread.sleep(100);
         }catch (InterruptedException e){
             e.printStackTrace();
         }
         stock.put(productId,stockNum);
     }
     
  	 //解锁
     redisLock.unlock(productId,String.valueOf(time));
}

实现之前首先我们来看下这两个命令的作用:
setnx:如果key不存在就跟set一样的作用,如果key存在则什么都不做
getandset: 返回上一次的value,并设置新的value

/**
 * redis分布式锁
 *
 * @author zhongxiaojian
 * @date 2019-07-30
 **/
@Component
@Slf4j
public class RedisLock {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 加锁
     * @param key   主键
     * @param value 当前时间+超时时间
     * @return
     */
    public boolean lock(String key,String value){
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(key,value);
        if (lock != null && lock){
            return true;
        }
        // currentValue = 1  这两个线程的value都是2  只有其中一个线程能获取锁
        String currentValue = redisTemplate.opsForValue().get(key);
        //如果锁过期
        if (!StringUtils.isEmpty(currentValue)
                && Long.parseLong(currentValue) < System.currentTimeMillis()){
            //获取上一个锁的时间
            String oldValue = redisTemplate.opsForValue().getAndSet(key,value);
            if (oldValue == null || (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue))){
                return true;
            }
        }
        return false;
    }

    /**
     * 解锁
     * @param key
     * @param value
     */
    public void unlock(String key,String value){
        try{
            String currentValue = redisTemplate.opsForValue().get(key);
            if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)){
                redisTemplate.opsForValue().getOperations().delete(key);
            }
        }catch (Exception e){
            log.error("【redis分布式锁】解锁异常,{}",e.getMessage());
        }
    }

}

这里我们来解释一下为何在lock方法当中加上 “//如果锁过期” 后面的代码

		// currentValue = 1  这两个线程的value都是2  只有其中一个线程能获取锁
        String currentValue = redisTemplate.opsForValue().get(key);
        //如果锁过期
        if (!StringUtils.isEmpty(currentValue)
                && Long.parseLong(currentValue) < System.currentTimeMillis()){
            //获取上一个锁的时间
            String oldValue = redisTemplate.opsForValue().getAndSet(key,value);
            if (oldValue == null || (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue))){
                return true;
            }
        }

假如我们不加上这段代码,那么在上面的使用方法sellProduct当中,在加锁之后的业务流程抛出了一个异常,且这个异常我们没有捕获并处理,那么我们接下来的解锁操作是不会执行的,这个时候我们的锁就变成了死锁,这个时候我们就可以使用getandset命令来进行解锁,我们来看一个例子:
假设一个购买B商品的线程发生了死锁,此时currentValue = 1,这个时候购买B商品的两个线程同时调用了lock方法,且value都等于2,同时这两个线程都进入了锁过期的判断"if (!StringUtils.isEmpty(currentValue)&& Long.parseLong(currentValue)

以上,就是我们使用redis实现了分布式锁。

你可能感兴趣的:(redis,后端,java,java)