怎么解决高并发下抢红包和商品超卖问题?

本文已参与「新人创作礼」活动,一起开启掘金创作之路。    

一、场景模拟

        在抢红包或秒杀商品的时候,肯定会有高并发的情况出现,程序中如果出现库存重复减扣的情况,那肯定是不行的!接下来模拟一下高并发下的库存重复减扣问题以及相应的解决方案。

       1.  在测试前,需要预先给redis设置一个key用来作为库存

  2. java代码如下: 

@GetMapping("/redis/stuck")
    public String getRedisStuck()  {
            //在高并发场景下会出现什么问题?
            int stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            String str="";
            if (stock>0){
                int realStock=stock-1;
                stringRedisTemplate.opsForValue().set("stock",realStock+"");
                str="扣减成功,剩余库存:"+realStock;
                System.out.println(str);
            }else{
                str="没有库存啦!";
                System.out.println(str);
            }
      return "end";
    }
复制代码

  3 . 使用nginx负载2个tomcat的架构,后面访问的时候在nginx里配了域名bingbing.com,注意在windows的hosts文件添加域名和ip的映射 192.168.254.10 bingbing.com

  nginx的配置参考:  nginx负载均衡多个tomcat实例_Dream_it_possible!的博客-CSDN博客_nginx负载均衡多个tomcat

  单线程下不会出现问题,但在高并发的情况下会出现什么问题呢?接下来使用压测工具Jmeter模拟高并发场景下,会出现的问题。

Jmeter配置

  1. 添加线程组,参数说明: 20个并发数,0表示立即执行,5表示循环5次。

 2. 添加接口:

 3. 启动Jmeter线程组,两台机器的打印结果如下:

      

  1. 可以发现,在并发量比较大的时候,出现库存没有扣除的情况!

二、解决方案

1. 加锁

使用setIfAsent()方法给key加锁,拿到锁了的线程才能执行。

   @GetMapping("/redis/stuck")
    public String getRedisStuck() throws IOException {
        String lockKey="product001";

        try{
            Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"华为手机");
            if (!result){
                return "请稍后重试!";
            }
            int stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            String str="";
            if (stock>0){
                //思考,如果执行在这时发生宕机时,怎么办?,finally又执行不了,key没有删除,其他线程不能拿到锁,产生死锁。
                int realStock=stock-1;
                stringRedisTemplate.opsForValue().set("stock",realStock+"");
                str="扣减成功,剩余库存:"+realStock;
                System.out.println(str);
            }else{
                str="没有库存啦!";
                System.out.println(str);
            }

        }finally {
            //解锁
            stringRedisTemplate.delete(lockKey);
        }
        
      return "end";
    }
复制代码

      存在的问题:  如果在if的时候出现程序宕机,拿到锁的key将会永远不会释放,出现死锁问题!

2. 设置失效时间 

        给锁添加失效时间,这样就不会出现用永远不会释放的问题。使用IfAbsent()方法同样可以设置失效时间!即拿到锁的线程可以继续执行,没拿到锁的线程先返回。修改如下代码:

 Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"华为手机",10 ,TimeUnit.SECONDS);
if (!result){ 
  return "请稍后重试!";
 }
复制代码

  完整代码,如下:

 @GetMapping("/redis/stuck")
    public String getRedisStuck() throws IOException {
        String lockKey="product001";
        try{
            //设置失效时间
            Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"华为手机",10 ,TimeUnit.SECONDS);
            //在高并发场景下会出现什么问题
            if (!result){
                return "请稍后重试!";
            }

            int stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            String str="";
            if (stock>0){
                //思考,如果执行在这时发生宕机时,怎么办?,finally又执行不了,key没有删除,其他线程不能拿到锁,产生死锁。
                int realStock=stock-1;
                stringRedisTemplate.opsForValue().set("stock",realStock+"");
                str="扣减成功,剩余库存:"+realStock;
                System.out.println(str);
            }else{
                str="没有库存啦!";
                System.out.println(str);
            }

        }finally {
            //解锁
                stringRedisTemplate.delete(lockKey);
        }

      return "end";
    }
复制代码

上述改动用redis锁失效的方法虽然在一定程度上解决了死锁问题, 但在高并发场景下仍然存在锁永久失效的问题!

3. 使用UUID

      分析: 当有线程进来的时候,给每一个线程生成一个UUID,然后将UUID存放到lockey的value里,释放锁前判断从redis拿到的锁对应的UUID是否与当前生成的UUID相等,如果相等,则释放锁!

    @GetMapping("/redis/stuck")
    public String getRedisStuck() throws IOException {
        String lockKey="product001";
        String clientId= UUID.randomUUID().toString();
        try{
            //设置失效时间
            Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10 ,TimeUnit.SECONDS);
            //在高并发场景下会出现什么问题
            if (!result){
                return "请稍后重试!";
            }

            int stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            String str="";
            if (stock>0){
                //思考,如果执行在这时发生宕机时,怎么办?,finally又执行不了,key没有删除,其他线程不能拿到锁,产生死锁。
                int realStock=stock-1;
                stringRedisTemplate.opsForValue().set("stock",realStock+"");
                str="扣减成功,剩余库存:"+realStock;
                System.out.println(str);
            }else{
                str="没有库存啦!";
                System.out.println(str);
            }

        }finally {
            //解锁
            if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
                stringRedisTemplate.delete(lockKey);
            }
        }

      return "end";
    }
复制代码

      此方法解决了线程释放其他线程的锁的可能!存在的问题:  程序的性能会比较低。

4. 强行给锁续命   

     思路: 有线程过来的时候,给当前线程开启一个分线程,分线程添加一个定时器,每隔锁失效时间的1/3时长来定时执行,然后判断key是否存在锁,如果存在锁那么给该锁的失效时间延长。

5 .redisson 分布式锁

      redisson分布式锁能够保证一个集群下只有一个线程能够拿到锁,而且性能高,分布式架构中首选使用此方法加锁。

    添加依赖:    

 <!--redisson分布式锁-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.6.5</version>
        </dependency>
复制代码

   启动的时候初始化,在主方法的类里,添加一个@Bean,注意: 如果redis有密码的话,需要设置密码。

  @Bean
    public Redisson redisson(){
        //初始化redisson客户端,单机模式
        Config config=new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0).setPassword("123456");
        return (Redisson)Redisson.create(config);
    }
复制代码

 使用的时候直接@autowire注解即可!

@Autowired
private Redisson redisson;
复制代码

在执行减库存前使用 lock()方法,执行lock方法的线程才能够继续往后执行,没有获取到lock的线程会进入阻塞状态。 

 @GetMapping("/redis/stuck")
    public String getRedisStuck() throws IOException {
      String lockKey="product001";
       RLock lock= redisson.getLock(lockKey);
        try{
            lock.lock(10,TimeUnit.SECONDS);
            int stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            String str="";
            if (stock>0){
                //思考,如果执行在这时发生宕机时,怎么办?,finally又执行不了,key没有删除,其他线程不能拿到锁,产生死锁。
                int realStock=stock-1;
                stringRedisTemplate.opsForValue().set("stock",realStock+"");
                str="扣减成功,剩余库存:"+realStock;
                System.out.println(str);
            }else{
                str="没有库存啦!";
                System.out.println(str);
            }
        }finally {
            //解锁
            lock.unlock();
        }
      return "end";
    }
复制代码

         redisson核心有一个watch dog机制,时存在锁的key 给延时失效时间,防止当前线程没有用完该key时,被其他线程抢去了该key的锁,这样可能会导致其他的业务问题。

猜你喜欢

转载自juejin.im/post/7103856244984643620