Redis解决秒杀中一人一单问题

前言

在上一篇,通过实例演示了在高并发场景下多人抢购优惠券的超卖问题,并且利用redis+lua解决了超卖问题,但是一人只能抢一单的问题是否还在呢?

一人抢多单压测

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可以发现,在经过改造时候,虽然解决了超卖问题,但是一人可以抢购多单的问题仍然存在

为什么会存在这个问题呢?简单分析下面这段代码,扣减库存和下单操作分割为2步,在多线程并发抢购时,第二步判断用户是否抢过和下面的第三步,第四步都是隔开的,

假设多个线程都走到第二步,这时候发现当前1111这个用户还没有抢过,继续走到第二步时候,当库存不为0时,从这一步到下单这一步,又是两个不同的步骤,因此多个线程存在交叉的情况,于是就出现了一人抢多单的情况
在这里插入图片描述

因此,理解了这一点,想必解决办法就出来了,将扣库存和下单的两步加锁不就可以解决吗?下面我们使用redis分布式锁进行处理,为简单起见,这里就直接使用redis比较简单的一种分布式锁redisson

1、添加依赖

		<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.10.0</version>
        </dependency>

2、使用redisson 的锁改造抢购代码

@Transactional(rollbackFor = Exception.class)
    public String doSkill(int vocherId,int userId){
        //1、判断代金券是否加入活动了
        String key = RedisKeyConstants.skill_vochers.getKey() + vocherId;
        Map<String,Object> entries = redisTemplate.opsForHash().entries(key);
        SkillVocher skillVocher = BeanUtil.mapToBean(entries,SkillVocher.class,true);
        if(skillVocher==null){
            throw new RuntimeException("该券未加入抢购活动");
        }
        //2、判断当前用户是否已经抢过
        VochersOrders defineOrder = vocherOrderMapper.findDefineOrder(userId, skillVocher.getId());
        if(defineOrder != null){
            throw new RuntimeException("该用户已经抢到过代金券,无需再抢了");
        }
        //3、扣减库存
        /*long count = redisTemplate.opsForHash().increment(key, "amount", -1);
        if(count < 0){
            throw new RuntimeException("该券已经卖完了");
        }*/
        String keyLock = RedisKeyConstants.skill_lock.getKey() + userId + ":" + vocherId;
        RLock lock = redissonClient.getLock(keyLock);
        lock.lock();
        try{
            //3、扣减库存采用 redis + lua
            List<String> keys = new ArrayList<>();
            keys.add(key);
            keys.add("amount");
            Long amount = (Long)redisTemplate.execute(defaultRedisScript, keys);
            if(amount == null || amount <1) {
                throw new RuntimeException("该券已经卖完了");
            }
            //4、下单
            VochersOrders vochersOrders = new VochersOrders();
            vochersOrders.setId(atomicInteger2.incrementAndGet());
            vochersOrders.setUserId(userId);
            vochersOrders.setSkillOrderId(skillVocher.getId());
            vochersOrders.setVocherId(skillVocher.getVocherId());
            String orderNo = UUID.randomUUID().toString().replaceAll("-","").substring(4,9);
            vochersOrders.setOrderNo(orderNo);
            vochersOrders.setOrderType(1);
            vochersOrders.setStatus(0);
            int saveCount = vocherOrderMapper.save(vochersOrders);
            if(saveCount !=0){
                return "skill success";
            }else {
                return "skill fail";
            }
        }catch (Exception e){
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            lock.unlock();
        }
        return "null";
    }

关于redisson实现分布式锁的讲解在我的另一篇博客中有提到,有兴趣的同学可以查阅,redisson提供了丰富已用的API供我们使用,相比其他的方式显得更简单而且容易上手,但在使用redisson的分布式锁的时候,最好使用tryLock并且带有尝试时间的那种方式

这样修改之后,我们再次使用jemeter的一人抢多单的线程组进行压测,观察数据结果

在这里插入图片描述
amount扣减1
在这里插入图片描述
数据库订单也只产生了一个
在这里插入图片描述

我们不妨再压测一次,最后发现数据仍然是正确的
在这里插入图片描述
在这里插入图片描述

通过上面的案例演示,采用redis的分布式锁解决了秒杀抢购中的一人一单的问题,有兴趣的同学可以继续深入探究,本篇到此结束,最后感谢观看!

猜你喜欢

转载自blog.csdn.net/zhangcongyi420/article/details/113897394