【商城秒杀项目】-- 使用rabbitmq异步下单

针对秒杀的业务场景,在高并发下,仅仅依靠页面缓存、对象缓存或者页面静态化等还是远远不够,数据库压力还是很大,所以需要异步下单,如果业务执行时间比较长,那么异步是最好的解决办法,但会带来一些额外的程序上的复杂性

具体思路:

  1. 系统初始化,把商品库存数量加载到Redis里面去
  2. 后端收到秒杀请求,Redis预减库存,如果库存已经到达临界值的时候,就不需要继续请求下去,直接返回失败,即后面的大量请求无需给系统带来压力
  3. 判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品,判断是否重复秒杀
  4. 库存充足,且无重复秒杀,将秒杀请求封装后放入消息队列,同时给前端返回一个code (0),即代表正在排队中(返回的并不是失败或者成功,此时还不能判断)
  5. 前端接收到数据后,显示排队中,并根据商品id轮询请求服务器(200ms轮询一次)
  6. 后端RabbitMQ监听秒杀MIAOSHA_QUEUE的这个名字的通道,如果有消息过来就获取到传入的信息,执行真正的秒杀之前,要判断数据库的库存,判断是否重复秒杀,然后执行秒杀事务(秒杀事务是一个原子操作:库存减1,下订单、写入订单详情)
  7. 此时,前端根据商品id轮询请求result接口查看是否生成了商品订单,如果返回-1代表秒杀失败,返回0代表排队中,返回>0代表秒杀成功

返回结果说明:
前端可根据后端返回的值来判断是否秒杀成功
-1 :库存不足,秒杀失败
0 :排队中,需继续轮询
>0 :返回的是商品id ,说明秒杀成功

本篇博客记录如何从rabbitmq中取消息进行下单,其他的业务逻辑可参考:

【商城秒杀项目】-- 秒杀的业务逻辑、接口的优化

监听rabbitmq,一旦有消息进入,就从该消息中获取对象进行秒杀操作

接收消息下单具体代码(MQReceiver.java):

package com.javaxl.miaosha_05.rabbitmq;

import com.javaxl.miaosha_05.domain.MiaoshaOrder;
import com.javaxl.miaosha_05.domain.MiaoshaUser;
import com.javaxl.miaosha_05.redis.RedisService;
import com.javaxl.miaosha_05.service.GoodsService;
import com.javaxl.miaosha_05.service.MiaoshaService;
import com.javaxl.miaosha_05.service.OrderService;
import com.javaxl.miaosha_05.vo.GoodsVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MQReceiver {

    private static Logger log = LoggerFactory.getLogger(MQReceiver.class);

    @Autowired
    RedisService redisService;

    @Autowired
    GoodsService goodsService;

    @Autowired
    OrderService orderService;

    @Autowired
    MiaoshaService miaoshaService;

    @RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = MQConfig.MIAOSHA_QUEUE,
                durable = "true"),
        exchange = @Exchange(value = MQConfig.MIAOSHA_EXCHANGE,
                durable = "true",
                type = "topic",
                ignoreDeclarationExceptions = "true"),
        key = "miaosha.#")
    )

    public void receive(String message) {
        log.info("receive message:" + message);
        //将string类型的message还原成bean,拿到了秒杀信息之后开始秒杀业务逻辑
        MiaoshaMessage mm = RedisService.stringToBean(message, MiaoshaMessage.class);
        MiaoshaUser user = mm.getUser();
        long goodsId = mm.getGoodsId();

        GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
        int stock = goods.getStockCount();
        //库存不足
        if (stock <= 0) {
            return;
        }
        //判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品
        MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
        if (order != null) {
            return;
        }
        //减库存 下订单 写入秒杀订单
        miaoshaService.miaosha(user, goods);
    }
}

下单相应的service层代码:

/**
 * 减库存、下订单、写入秒杀订单
 */
@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
    try {
        //减数据库里该商品的库存
        goodsService.reduceStock(goods);
        //下订单、写入秒杀订单
        return orderService.createOrder(user, goods);
    }catch (Exception e){
        e.printStackTrace();
        //还原redis里的库存
        redisService.incr(GoodsKey.getMiaoshaGoodsStock, "" + goods.getId());
        throw new RuntimeException();
    }
}

执行秒杀事务的时候,先生成订单详情,然后生成秒杀订单,为了进一步确保秒杀过程中一个用户只能秒杀一件商品,可以给秒杀订单表miaosha_order添加一个唯一索引,如果再次插入相同的user_id与goods_id,那么将不会被允许,从而在事务中插入失败而回滚:

reduceStock方法代码:

相应的dao层代码:

createOrder方法代码:

相应的dao层代码:

注:秒杀操作是一个事务,需使用@Transactional注解来标识,如果减少库存失败,则回滚

前端根据商品id轮询请求result接口,查看是否生成了商品订单并判断是否秒杀成功

获取秒杀结果接口代码:

/**
 * orderId:成功
 * -1:秒杀失败
 * 0: 排队中
 */
@RequestMapping(value = "/result", method = RequestMethod.GET)
@ResponseBody
public Result<Long> miaoshaResult(HttpServletRequest request, HttpServletResponse response, Model
        model, MiaoshaUser user, @RequestParam("goodsId") long goodsId) {
    model.addAttribute("user", user);
    if (user == null) {
        return Result.error(CodeMsg.SESSION_ERROR);
    }
    long result = miaoshaService.getMiaoshaResult(user.getId(), goodsId);
    return Result.success(result);
}

getMiaoshaResult方法代码:

getMiaoshaOrderByUserIdGoodsId方法代码:

前端轮询result接口的代码:

result这个接口,从缓存中拿订单(将订单信息录入数据库的同时还会往redis放一份),如果有则返回商品id,说明秒杀成功;商品卖完了则返回-1,说明秒杀失败;商品没有卖完则返回0,说明正在排队中,需继续轮询,前端拿到返回的数据,通过判断,进行显示,成功就跳转订单页面

发布了133 篇原创文章 · 获赞 94 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/weixin_42687829/article/details/104497733