唐僧喜欢小龙女

导航

分布式锁的解决

1、分布式锁的使用场景

  • 秒杀类的
  • 接口幂等性

2、秒杀问题现象

通过Nginx 配置了负载,访问后台的接口,接口里面减库存代码如下

@RestController
public class RedisController {
   @Autowired
   private StringRedisTemplate stringRedisTemplate;


    @RequestMapping("/deductStock")
    public String deductStock(){

        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        if(stock>0){
           int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock",realStock+"");
            System.out.println("库存扣减成功、剩余库存:"+realStock);
        }else {
            System.out.println("扣减失败库存不足");
        }

        return "end";

    }
}

测试结果如下

可以发现再并发的情况下会出现超卖的情况。

3、使用原生Redis的方式解决

/**
 *   使用redis 做分布式锁
 *   1、Redis 是单线程还是多线程---->单线程
 *
 */
@RestController
public class RedisController {
   @Autowired
   private StringRedisTemplate stringRedisTemplate;


    @RequestMapping("/deductStock")
    public String deductStock(){

        String stockLock = "stockLock";


        String  clientId = UUID.randomUUID().toString();
        try{
            /**
               setIfAbsent 的意思是 如果key存在的话,那么设置值是无法成功的
               Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(stockLock,"gaohq");
               可以给锁设置一个超时时间(10秒),就算是jvm宕机了 这个锁也可以释放,这里也要特别的注意啊,
               上面的加锁了后,下面的设置超时时间没有执行,jvm 宕机了。也会造成死锁
               也就是需要设置值和设置超时时间为一个原子操作才行,
               stringRedisTemplate.expire(stockLock,10, TimeUnit.SECONDS);

               这里设置value的时候不要写死了,如果第一个请求过来 耗时15秒,第二个请求过来耗时10秒
               第一个请求到 if(stock>0) 这里耗了10秒了 剩下的代码执行需要 5秒,此时value 已经超时了,
               第二个线程就进来了,获得了锁。在finally 里面判断下。

               这个时间设置多久呢
                  设置多久都不合适、需要锁的续命的方式。 手写太麻烦,用Redission 就行。

           **/
            //下面这行代码是原子操作 也就是说放值和设置超时时间是要一个原子操作。
            //Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(stockLock,"gaohq",10,TimeUnit.SECONDS);
            Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(stockLock,clientId,10,TimeUnit.SECONDS);

            if(!result){
                return "error code";
            }
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if(stock>0){
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock",realStock+"");
                System.out.println("库存扣减成功、剩余库存:"+realStock);
            }else {
                System.out.println("扣减失败库存不足");
            }
            //如果这里获取到了锁后,然后突然程序被kill 掉了,那么finally里面也就不能执行了。造成死锁
        }finally {

            /**
             * 如果上面的代码抛异常了,那么stockLock 这个锁就一直存在存在死锁,那么就扣不了库存了
             * 像这种关闭链接、释放锁、关闭流 一般都放到finally 里面。
             * 释放锁的时候要判断下,当前请求也就是线程 释放的锁是否是当前线程拿到的,避免因为时间的问题
             * 把别的线程的拿到的锁释放了。
             * 
             *
             * 释放锁,涉及到三个步骤,获取锁、比较锁、释放锁三个步骤,这个三个步骤明显不是一个原子操作,可以使用lua脚本
             * 在redis的服务端原子性的执行多个redis的命令。  
             *
             */
            if(clientId.equals(stringRedisTemplate.opsForValue().get("stockLock"))){
                stringRedisTemplate.delete(stockLock);
            }
        }
        return "end";

    }
}

 

 

4、Redisson 的方式

4.1 Redisson的配置类

/**
 *
 * 这个类用来把一些不在springboot自动包扫描的路径下的类,放到容器里面。
 *
 */
@Import({Person.class, Book.class, AgeMyServiceImpl.class, QueryGrantTypeService.class})
//@ComponentScan("com.ali.gits.beans")
@Configuration
public class GaohqClassConfig {
    @Value("${redis.hostname}")
    String hostName;

    @Value("${redis.port}")
    int port;

    @Value("${spring.redis.password}")
    String password;

    @Bean(destroyMethod="shutdown")
    RedissonClient redisson() throws IOException {
        //1、创建配置
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://"+hostName+":"+port).setPassword(password);

        return Redisson.create(config);
    }
}

4.2 使用Redisson

/**
 *
 *   使用Redission 解决分布式锁的问题。
 *
 */
@RestController
public class RedissionController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    @RequestMapping("/deductStockByRedisson")
    public String deductStock(){

        String stockLock = "stockLock";
        RLock rLock = redissonClient.getLock(stockLock);
        rLock.lock();

        try{
            //如果业务执行过长,Redisson会自动给锁续期
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if(stock>0){
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock",realStock+"");
                System.out.println("库存扣减成功、剩余库存:"+realStock);
            }else {
                System.out.println("扣减失败库存不足");
            }
            //如果这里获取到了锁后,然后突然程序被kill 掉了,那么finally里面也就不能执行了。造成死锁
        }finally {
            rLock.unlock();
        }
        return "end";

    }


}

 

posted on 2022-02-28 17:39  与时具进&不忘初心  阅读(48)  评论(0编辑  收藏  举报