Redis实现分布式锁

254 阅读2分钟

是针对某个资源的状态,保证其访问的互斥性,在实际使用当中,这个状态一般是一个字符串。使用 Redis 实现锁,主要是将状态放到 Redis 当中,利用其原子性,当其他线程访问时,如果 Redis 中已经存在这个状态,就不允许之后的一些操作。spring boot使用Redis的操作主要是通过RedisTemplate(或StringRedisTemplate )来实现。

现在我们来用spring boot + redis来实现Redis分布式锁

1.首先我们引用Spring-boot所带的Redis的依赖

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

2. 了解Redis加锁主要命令

  • SETNX(SET if Not exist):当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。
  • GETSET:将给定 key 的值设为 value ,并返回 key 的旧值。先根据key获取到旧的value,再set新的value。
  • EXPIRE 为给定 key 设置生存时间,当 key 过期时,它会被自动删除。

3. Redis加锁

   /**
     * 加锁
     * @param key 锁唯一标志
     * @param value当前时间 + 超时时间
     * @return
     */
    public boolean lock(String key, String value){
 
        if(stringRedisTemplate.opsForValue().setIfAbsent(key,value)){
            return true;
        }
        /**
         * 以下代码是防止加锁后出现操作异常,没有运行解锁操作  防止死锁
         */
        //获取锁的过期时间
        String currentValue = (String)stringRedisTemplate.opsForValue().get(key);
        //假设锁过期
        if(!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()){
            //获取上一个锁的时间,并设置新锁的时间
            String oldValue = (String) stringRedisTemplate.opsForValue().getAndSet(key,value);
             //校验是否和上一个锁时间戳相同,如果相同才有权利加锁
            if(!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue) ){
                return true;
            }
        }
        return false;

4. Redis解锁

    /**
     * 解锁
     * @param key
     * @param time
     */
    public void unlock(String key,String time){
        try {
            // 获取锁的时间戳
            String currentValue = stringRedisTemplate.opsForValue().get(key);
            // 判断传进来的时间戳与锁的时间戳是否相同
            if(!Strings.isNullOrEmpty(currentValue) && currentValue.equals(time) ){
                // 相同则进行删除锁
                stringRedisTemplate.opsForValue().getOperations().delete(key);
            }
        } catch (Exception e) {
            log.error("解锁失败,异常{}",e);
        }
    }

5.模拟秒杀场景

@RestController
@Slf4j
public class RedisController {
    @Autowired
    private RedisLock  redisLock;
    /**
     *  设置超时时间 5s
     */
    private static final int TIMEOUT = 5 * 1000;

    @Override
    public void Spike(String productId){
        //加锁
        long time = System.currentTimeMillis() + TIMEOUT;
        if(!redisLock.lock(productId,String.valueOf(time))){
            return "排队人数太多,请稍后再试.";
        }
        int stockNum = stock.get(productId);
        // 查询该商品库存,为0则活动结束 
        if(stockNum==0){
           throw new OperationFailedException("活动结束");
        }else {
            // 下单(模拟不同的openId)
            orders.put(KeyUtil.getOpenId(),productId);
            //减库存 不做处理的话,高并发下会出现超卖的情况,下单数,大于减库存的情况。
            //虽然这里减了,但由于并发,减的库存还没存到map中去。新的并发拿到的是原来的库存
            stockNum -= 1;
            try{
                Thread.sleep(100);//模拟减库存的处理时间
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            // 存储最新库存数量
            stock.put(productId,stockNum);
        }
        //解锁
        redisLock.unlock(productId,String.valueOf(time));
    }
}

注:测试并发量可以采用Apache ab并发负载压力进行测试