高并发秒杀实战教学

它来了,它来了,它带着高并发走来了。

现在网络越来越发达,网民越来越多,直播带货越来越多,所以我觉得非常有必要学习下高并发技术。

解决高并发问题有很多种方法,我这里用到的主要技术是 RabbitMQ + Redis

首先介绍下 整体的高并发流程。

我们这个过程全程数据库都不参与,全程使用Redis来代替,这样就大大的提高了并发能力。因为Redis只基于内存的操作,非常快。

第一步、上架商品

代码有点多, 是因为我的项目逻辑比较复杂。

主要功能是:

        1、把活动和对应的skuId保存到redis

        2、把每个sku商品的库存保存到redis

        3、把每个sku商品的信息保存到redis

 /**
     * 1、加锁
     * 2、保证幂等性
     */
    @Scheduled(cron =" */10 * * * * ?")
    public void uploadSeckillSkuLatest3Days() {
        log.info("上架秒杀商品。。。");
        RLock lock = redissonClient.getLock(upload_lock);
        try {
            lock.lock(10, TimeUnit.SECONDS);
            seckillService.uploadSeckillSkuLatest3Days();
        }catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    private final String SESSION__CACHE_PREFIX = "seckill:sessions:";

    private final String SECKILL_CHARE_PREFIX = "seckill:skus";

    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";    //+商品随机码




@Override
    public void uploadSeckillSkuLatest3Days() {
        //1、扫描最近三天的商品需要参加秒杀的活动
        R lates3DaySession = couponFeignService.getLates3DaySession();
        if (lates3DaySession.getCode() == 0) {
            //上架商品
            List<SeckillSessionWithSkusVo> sessionData = (List<SeckillSessionWithSkusVo>) lates3DaySession.getData("data", new TypeReference<List<SeckillSessionWithSkusVo>>() {
            });
            //缓存到Redis
            //1、缓存活动信息
            saveSessionInfos(sessionData);
            //2、缓存活动的关联商品信息
            saveSessionSkuInfo(sessionData);
        }
    }

    
/**
     * 缓存秒杀活动信息
     * @param sessions
     */
    private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) {

        if (CollectionUtils.isEmpty(sessions)) {
            return;
        }
        sessions.stream().forEach(session -> {

            //获取当前活动的开始和结束时间的时间戳
            long startTime = session.getStartTime().getTime();
            long endTime = session.getEndTime().getTime();

            //存入到Redis中的key
            String key = SESSION__CACHE_PREFIX + startTime + "_" + endTime;

            //判断Redis中是否有该信息,如果没有才进行添加
            Boolean hasKey = redisTemplate.hasKey(key);
            //缓存活动信息
            if (!hasKey) {
                //获取到活动中所有商品的skuId
                List<String> skuIds = session.getRelationSkus().stream()
                        .map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList());
                redisTemplate.opsForList().leftPushAll(key,skuIds);
            }
        });

    }

/**
     * 缓存秒杀活动所关联的商品信息
     * @param sessions
     */
    private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) {
        if (CollectionUtils.isEmpty(sessions)) {
            return;
        }
        sessions.stream().forEach(session -> {
            //准备hash操作,绑定hash
            BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
            session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                //生成随机码
                String token = UUID.randomUUID().toString().replace("-", "");
                String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString();
                if (!operations.hasKey(redisKey)) {

                    //缓存我们商品信息
                    SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
                    Long skuId = seckillSkuVo.getSkuId();
                    //1、先查询sku的基本信息,调用远程服务
                    R info = productFeignService.getSkuInfo(skuId);
                    if (info.getCode() == 0) {
                        SkuInfoVo skuInfo = (SkuInfoVo) info.getData("skuInfo",new TypeReference<SkuInfoVo>(){});
                        redisTo.setSkuInfo(skuInfo);
                    }

                    //2、sku的秒杀信息
                    BeanUtils.copyProperties(seckillSkuVo,redisTo);

                    //3、设置当前商品的秒杀时间信息
                    redisTo.setStartTime(session.getStartTime().getTime());
                    redisTo.setEndTime(session.getEndTime().getTime());

                    //4、设置商品的随机码(防止恶意攻击)
                    redisTo.setRandomCode(token);

                    //序列化json格式存入Redis中
                    String seckillValue = JSON.toJSONString(redisTo);
                    operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue);

                    //如果当前这个场次的商品库存信息已经上架就不需要上架
                    //5、使用库存作为分布式Redisson信号量(限流)
                    // 使用库存作为分布式信号量
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                    // 商品可以秒杀的数量作为信号量
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
                }
            });
        });
    }

结果:

 

上架就完成。

第二步、抢购

        直接上代码了,代码中有详细注释

@GetMapping("/kill")
    @ResponseBody
    public R kill(@RequestParam("killId") String killId,
                  @RequestParam("key") String key,
                  @RequestParam("num") Integer num,
                  Model model) throws InterruptedException {

        String orderSn = seckillService.kill(killId,key,num);
        return R.ok().setData(orderSn);
    }
public String kill(String killId, String key, Integer num) throws InterruptedException {
        long l1 = System.currentTimeMillis();
        MemberResponse memberResponse = LoginUserInterceptor.loginUser.get(); // 线程直接获取用户信息
        // 1、  redis中获取商品数据
        BoundHashOperations<String, String, String> hasOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
        String skuInfo = hasOps.get(killId);
        if (StringUtils.isEmpty(skuInfo)) {
            return null;
        }
        // 2、 简单校验、防止恶意请求
        SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfo, SeckillSkuRedisTo.class);
        // 活动的开始结束时间
        Long startTime = redisTo.getStartTime();
        Long endTime = redisTo.getEndTime();
        long currentTime = System.currentTimeMillis();
        // 3、判断当前时间是否在秒杀时间段内
        if (currentTime < startTime || currentTime > endTime) {
            return null;
        }
        //4、效验随机码和商品id
        String randomCode = redisTo.getRandomCode();
        String skuId = redisTo.getPromotionSessionId() + "-" + redisTo.getSkuId();
        if (!killId.equals(skuId) || !key.equals(randomCode)) {
            // 校验失败,返回null
            return null;
        }
        // 5、 暂时设定每个人只能抢购一次, 防止脚本疯狂刷接口
        // redis  key
        String redisKey = memberResponse.getId() + "-" + skuId;
        //  redis 时间
        long ttl = endTime - startTime;
        // set nx ex
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
        if (!flag) {
            // 已经抢购过了
            return null;
        }
        // 6、 使用信号量原子性,减库存
        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
        boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
        // 减库存是否成功
        if (!semaphoreCount) {
            // 没成功
            return null;
        }
        //7、创建订单号和订单信息发送给MQ
        String orderSn = IdWorker.getTimeId();
        SeckillOrderTo seckillOrderTo = new SeckillOrderTo();
        seckillOrderTo.setOrderSn(orderSn);
        seckillOrderTo.setNum(num);
        seckillOrderTo.setMemberId(memberResponse.getId());
        seckillOrderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
        seckillOrderTo.setSkuId(redisTo.getSkuId());
        seckillOrderTo.setSeckillPrice(redisTo.getSeckillPrice());
        // 发消息。订单服务去处理
        rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", seckillOrderTo);

        long l2 = System.currentTimeMillis();
        log.info("秒杀耗时...." + (l2 - l1));
        return orderSn;
    }

第三步、监听消息

        先创建好我们的MQ中的队列、交换机和绑定关系。

 /**
     * 商品秒杀队列
     * @return
     */
    @Bean
    public Queue orderSecKillOrrderQueue() {
        Queue queue = new Queue("order.seckill.order.queue", true, false, false);
        return queue;
    }

    @Bean
    public Binding orderSecKillOrrderQueueBinding() {
        Binding binding = new Binding(
                "order.seckill.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.seckill.order",
                null);

        return binding;
    }

       创建监听

@RabbitHandler
    public void listener(SeckillOrderTo orderTo, Channel channel, Message message) throws IOException {
        log.info("开始创建秒杀订单信息》。。。");
        try {
            orderService.createSeckillOrderInfo(orderTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (IOException e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
            throw new RuntimeException(e);
        }
    }

@Override
    @Transactional
    public void createSeckillOrderInfo(SeckillOrderTo orderTo) {
        //  保存订单信息
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setOrderSn(orderTo.getOrderSn());
        orderEntity.setMemberId(orderTo.getMemberId());
        orderEntity.setCreateTime(new Date());
        BigDecimal totalPrice = orderTo.getSeckillPrice().multiply(BigDecimal.valueOf(orderTo.getNum()));
        orderEntity.setPayAmount(totalPrice);
        orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
        //保存订单
        this.save(orderEntity);

        //保存订单项信息
        OrderItemEntity orderItem = new OrderItemEntity();
        orderItem.setOrderSn(orderTo.getOrderSn());
        orderItem.setRealAmount(totalPrice);
        orderItem.setSkuId(orderTo.getSkuId());
        orderItem.setSkuQuantity(orderTo.getNum());

        // todo  1
        // 保存商品的spu信息
        // 都是一样的逻辑,远程调用一下商品服务就可以了
        // .......


        // todo  2
        // 地址可以先使用用户的默认地址, 当抢购成功后,用户可以再进行修改地址信息
        // .....


        //保存订单项数据
        orderItemService.save(orderItem);
    }

结果:

总结

核心思想就是使用Redis中的信号量来控制减库存,完成快速秒杀。

RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
semaphore.trySetPermits(seckillSkuVo.getSeckillCount());

这样一个最简单的高并发下单减库存逻辑就完成了。看一下我本机测试的结果:

我本机秒杀一次,秒杀接口的处理速度是7ms, 我们把中间的网管转发,网络传输时间再加上,一次秒杀的请求大约可以是15ms,那么我们单个tomcat至少能够支持1000线程的并发,那么我自己这台电脑现在1秒钟能够支持的并发就达到了 1*1000ms/15ms*1000 = 6.666万。所以如果想要达到百万并发,我们可以提升服务器硬件质量或者集群部署就能轻松达到。

  • 20
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值