为何需要异步下单

在秒杀系统用户进行抢购的过程中,由于在同一时间会有大量请求涌入服务器,如果每个请求都立即访问数据库进行扣减库存+写入订单的操作,对数据库的压力是巨大的。

如何减轻数据库的压力呢,我们将每一条秒杀的请求存入消息队列(例如RabbitMQ)中,放入消息队列后,给用户返回类似“抢购请求发送成功”的结果。而在消息队列中,我们将收到的下订单请求一个个的写入数据库中,比起多线程同步修改数据库的操作,大大缓解了数据库的连接压力,最主要的好处就表现在数据库连接的减少:

  • 同步方式:大量请求快速占满数据库框架开启的数据库连接池,同时修改数据库,导致数据库读写性能骤减。
  • 异步方式:一条条消息以顺序的方式写入数据库,连接数几乎不变(当然,也取决于消息队列消费者的数量)。

这种实现可以理解为是一种流量削峰:让数据库按照他的处理能力,从消息队列中拿取消息进行处理。

简洁逻辑处理

具体分为4个流程:
1、获取库存标记,预减库存时,当库存小于0的情况下,库存标记变为true,当库存小于零则直接结束,不操作redis
2、Redis预减库存,在系统初始化时将商品库存信息加载到缓存中,如果预减库存小于零则直接结束。
3、判断用户是否是第一次秒杀,在创建订单时会在redis中生成一个副本,同样是操作缓存,避免用户重复秒杀。
4、秒杀入队,将用户与商品ID传递给消息队列进行处理。

继承InitializingBean并实现afterPropertiesSet()方法, 凡是继承该接口的类,在初始化bean的时候都会执行该方法。

/**
     * 系统每次初始化时,将秒杀商品的库存数量加载到redis
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        List<GoodsList> goodsList = goodsService.getGoodsList();
        if(goodsList == null){
            return;
        }
        for(GoodsList good : goodsList){
            isOver.put(good.getId(),false);
            Integer stock_count = good.getStock_count();
            redisService.set(GoodListKey.miaoshaGoodCountKey,good.getId()+"",stock_count);
        }
    }

秒杀接口:

    @RequestMapping("/miaosha_static")
    @ResponseBody
    public Result<Integer> miaoSha_static(@RequestParam("good_id") int good_id, @UserParameter User user){

        //库存标记,如果库存小于零则直接结束,不操作redis
        if(isOver.get(good_id)){
            return Result.error(CodeMsg.OUT_OF_STOCK);
        }

        //判断是否还有库存
        //预减库存
        Long decr = redisService.decr(GoodListKey.miaoshaGoodCountKey, good_id + "");
        if(decr < 0){
            isOver.put(good_id,true);
            return Result.error(CodeMsg.OUT_OF_STOCK);
        }

        //判断该用户是第一次秒杀,不可重复秒杀
        //查找redis缓存,绕开数据库
        Miaosha_order miaosha_order = redisService.get(MiaoSha_OrderKey.orderKey,user.getId()+":"+good_id,Miaosha_order.class);
        if(miaosha_order != null){
            return Result.error(CodeMsg.NO_REPEAT_MIAOSHA);
        }

        MiaoshaMsg msg = new MiaoshaMsg();
        msg.setUser(user);
        msg.setGood_id(good_id);
        //秒杀入队
        provider.miaoshaProvider(msg);
        return Result.success(0);  //0代表排队中
    }

接口将用户信息与商品ID传递给消息的Provider,然后消费者监听到队列里有了消息,进行消费。

provider:

public void miaoshaProvider(MiaoshaMsg msg){
        String message = redisService.beanToString(msg);
        System.out.println("miaoshaProvider");
        amqpTemplate.convertAndSend(MqConfig.MIAOSHA_QUEUE,message);
    }

Consumer:

    @RabbitListener(queues = MqConfig.MIAOSHA_QUEUE)
    public void miaoshaConsumer(String msg){
        MiaoshaMsg message = redisService.StringToBean(msg, MiaoshaMsg.class);
        GoodsList good = goodsService.getGoodsListById(message.getGood_id());
        //判断该商品是否还有库存
        int stock_count = miaoSha_goodsService.getMiaosha_goodsByid(good.getId());
        if(stock_count <= 0){
            return;
        }

        //判断该用户是第一次秒杀,不可重复秒杀
        //查找redis缓存,绕开数据库
        Miaosha_order miaosha_order = redisService.get(MiaoSha_OrderKey.orderKey,message.getUser().getId()+":"+good.getId(),Miaosha_order.class);
        if(miaosha_order != null){
            return;
        }

        //允许秒杀
        //库存-1 生成order_info订单 生成秒杀订单
        miaoShaService.miaoSha(message.getUser(), good);
    }

注意:消息队列生产和消费时接收的消息都必须是String类型,所以在执行逻辑前需要BeanToStringStringToBean进行转换

事务进行订单生成:

 @Transactional
    public Integer miaoSha(User user, GoodsList good){
        //减少库存
        int i = miaoShaGoodsService.reduceStockCount(good.getId());
        if(i == 1){
            //减库存成功
            //创建Order_info订单,秒杀订单
            int order_id = order_infoService.createOrder_Info(user, good);
            return order_id;
        }else {
            //减库存失败,无库存
            setGoodsCountIsOver(good.getId());
            return null;
        }
    }

减库存失败时调用setGoodsCountIsOver()将该商品ID存放到redis中代表此商品已无库存

public void setGoodsCountIsOver(int good_id){
    redisService.set(GoodListKey.miaoshaGoodCountIsOver,good_id+"",true);
}

创建订单的过程中同样在Redis中生成一个副本

 @Transactional
    public int createOrder_Info(User user, GoodsList good){
        Order_info order_info = new Order_info();
        order_info.setUser_id(user.getId());
        order_info.setGoods_id(good.getId());
        order_info.setDelivery_addr_id(1);
        order_info.setGoods_name(good.getGoods_name());
        order_info.setGoods_count(good.getStock_count());
        order_info.setGoods_price(good.getMiaosha_price());
        order_info.setOrder_channel(1);
        order_info.setStatus(0);
        order_info.setCreate_date(new Date());
        order_info.setPay_date(null);
        //创建订单
        order_infoDao.createOrder_info(order_info);
        int order_id = order_info.getId();
        //创建秒杀订单
        miaoSha_orderService.createMiaoSha_Order(user, good, order_id);

        Miaosha_order miaosha_order = new Miaosha_order();
        miaosha_order.setUser_id(user.getId());
        miaosha_order.setOrder_id(order_id);
        miaosha_order.setGoods_id(good.getId());
        //订单存入缓存
        redisService.set(MiaoSha_OrderKey.orderKey,user.getId()+":"+good.getId(),miaosha_order);
        return order_id;
    }

前端逻辑处理的过程中,当秒杀接口返回的code0时,代表秒杀逻辑执行成功,然后ajax去异步执行一个请求判断订单是否已经生成,若订单已生成则返回订单的ID代表秒杀成功,否则查询该商品是否已经无库存,若已无库存则返回-1代表秒杀失败,若还有库存则返回0代表该请求还在排队中。

/**
     * orderId 秒杀成功
     * 0 排队中
     * -1 失败
     */
    @RequestMapping("/getmiaoshadetail")
    @ResponseBody
    public Result<Integer> getMiaoshaDetail(@UserParameter User user,@RequestParam("good_id") int good_id){
        int detail = miaoShaService.getMiaoshaDetail(user, good_id);
        return Result.success(detail);
    }
public int getMiaoshaDetail(User user,int good_id){
        Miaosha_order order = redisService.get(MiaoSha_OrderKey.orderKey,user.getId()+":"+good_id,Miaosha_order.class);
        if(order != null){
            //秒杀成功
            return order.getOrder_id();
        }else {
            boolean over = getGoodsCountIsOver(good_id);
            if(over){
                //无库存,秒杀失败
                return -1;
            }else {
                //排队中
                return 0;
            }
        }
    }
public boolean getGoodsCountIsOver(int good_id){
       return redisService.exist(GoodListKey.miaoshaGoodCountIsOver,good_id+"");
    }

前端反馈给用户的信息就可以自己设计,如何给用户的良好体验感,贴出来篇幅过长,这里就不展示啦!

最后修改:2021 年 11 月 15 日
如果觉得我的文章对你有用,请随意赞赏