五、Rabbitmq秒杀优化

思路:减少数据库访问

  • 系统初始化,把商品库存数量加载到redis
  • 收到请求,redis预减库存,库存不够,直接返回,否则进入3
  • 请求入队,立即返回排队中
  • 请求出队,生成订单,减少库存
  • 客户端轮询,是否秒杀成功

对于之前的秒杀接口do_miaosha:

@RequestMapping(value = "/do_miaosha",method = RequestMethod.POST)
@ResponseBody
public Result<Integer> do_miaosha(Model model, MiaoshaUser user, @RequestParam("goodsId") long goodsId){
    if(user == null)
        return Result.error(CodeMsg.SESSION_ERROR);
    //判断库存
    GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId);
    if(goodsVo.getStockCount() <= 0){
        return Result.error(CodeMsg.MIAO_SHA_OVER);
    }


    //判断是否已经秒杀到了
    MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(),goodsId);
    if(miaoshaOrder != null){
        return Result.error(CodeMsg.REPEATE_MIAOSHA);
    }


    //减库存、下订单、写入秒杀订单,需要在一个事务中执行
    OrderInfo orderInfo = miaoshaService.miaosha(user,goodsVo);

    return Result.success(orderInfo);
}

这里判断库存是直接从数据库查,因为并发量比较大,存在性能问题。后面秒杀到之后,也不是直接减库存, 而是将其放到消息队列中慢慢交给数据库去调整。

@RequestMapping(value = "/do_miaosha",method = RequestMethod.POST)
@ResponseBody
public Result<Integer> do_miaosha(Model model, MiaoshaUser user, @RequestParam("goodsId") long goodsId){
    if(user == null)
        return Result.error(CodeMsg.SESSION_ERROR);

    //1.预减库存进行优化
    /*********************************优化1开始*************************************/
    long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId);
    if(stock < 0){
        return Result.error(CodeMsg.MIAO_SHA_OVER);
    }
    /*********************************优化1结束*************************************/
    
    //2.判断是否已经秒杀到了
    MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(),goodsId);
    if(miaoshaOrder != null){
        return Result.error(CodeMsg.REPEATE_MIAOSHA);
    }
    /*********************************优化2开始*************************************/
    //3.进入消息队列
    MiaoshaMessage message = new MiaoshaMessage();
    message.setUser(user);
    message.setGoodsId(goodsId);
    sender.sendMiaoshaMessage(message);
    /*********************************优化2结束*************************************/
    return Result.success(0);//排队中
}

这是sender

@Autowired
	AmqpTemplate amqpTemplate ;
	
	public void sendMiaoshaMessage(MiaoshaMessage mm) {
		String msg = RedisService.beanToString(mm);
		log.info("send message:"+msg);
		amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
	}

在消息队列中对消息进行消化:

@RabbitListener(queues = MQConfig.MIAOSHA_QUEUE)
public void receive(String message){
    log.info("receive message:{}",message);
    MiaoshaMessage msg = RedisService.stringToBean(message,MiaoshaMessage.class);
    MiaoshaUser user = msg.getUser();
    long goodsId = msg.getGoodsId();
    //判断数据库库存是否真的足够
    GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId);
    if(goodsVo.getStockCount() <= 0){
        return;
    }
    //判断是否已经秒杀到了
    MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(),goodsId);
    if(miaoshaOrder != null){
        return;
    }
    //减库存、下订单、写入秒杀订单,需要在一个事务中执行
    OrderInfo orderInfo = miaoshaService.miaosha(user,goodsVo);

}

对于controller中的优化1:redis预减库存。那么需要在系统启动的时候将秒杀商品的库存先添加到redis中:

public class MiaoshaController implements InitializingBean

重写afterPropertiesSet()方法:

	/**
	 * 系统初始化
	 * */
	public void afterPropertiesSet() throws Exception {
		List<GoodsVo> goodsList = goodsService.listGoodsVo();
		if(goodsList == null) {
			return;
		}
		for(GoodsVo goods : goodsList) {
			redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount());
			localOverMap.put(goods.getId(), false);
		}
	}

对于前端,这时也要进行修改了,因为点击秒杀商品按键后,这里考虑三种情况:排队等待、失败、成功。那么这里规定-1为失败,0为排队,1为秒杀成功已经写入数据库
原来的detail.htm中秒杀事件函数:

function doMiaosha(){
	$.ajax({
		url:"/miaosha/do_miaosha",
		type:"POST",
		data:{
			goodsId:$("#goodsId").val(),
		},
		success:function(data){
			if(data.code == 0){
				window.location.href="/order_detail.htm?orderId="+data.data.id;
			}else{
				layer.msg(data.msg);
			}
		},
		error:function(){
			layer.msg("客户端请求有误");
		}
	});
	
}

秒杀到商品就直接返回,现在后端改为消息队列,所以需要增加函数进行判断,必要时需要轮询
所以将其改为:

if(data.code == 0){
	//window.location.href="/order_detail.htm?orderId="+data.data.id;
    //秒杀到商品的时候,这个时候不是直接返回成功,后端是进入消息队列,所以前端是轮询结果,显示排队中
    getMiaoshaResult($("#goodsId").val());
}else{
	layer.msg(data.msg);
}

function getMiaoshaResult(goodsId) {
    g_showLoading();
    $.ajax({
        url:"/miaosha/result",
        type:"GET",
        data:{
            goodsId:$("#goodsId").val(),
        },
        success:function(data){
            if(data.code == 0){
                var result = data.data;
                //失败---    -1
                if(result <= 0){
                    layer.msg("对不起,秒杀失败!");
                }
                //排队等待,轮询---   0
                else if(result == 0){//继续轮询
                    setTimeout(function () {
                        getMiaoshaResult(goodsId);
                    },50);
                }
                //成功----   1
                else {
                    layer.msg("恭喜你,秒杀成功,查看订单?",{btn:["确定","取消"]},
                        function () {
                            window.location.href="/order_detail.htm?orderId="+result;
                        },
                        function () {
                            layer.closeAll();
                        }
                    );
                }
            }else{
                layer.msg(data.msg);
            }
        },
        error:function(){
            layer.msg("客户端请求有误");
        }
    });
}

那么相应地,后台也要增加一个方法:result

    /**
     * orderId:成功
     * -1:秒杀失败
     * 0: 排队中
     * */
    @RequestMapping(value="/result", method=RequestMethod.GET)
    @ResponseBody
    public Result<Long> miaoshaResult(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方法所做的事情。

对于成功的状态判断,很简单,从数据库查,能查到就说明已经秒杀成功,否则就是两种情况:失败或者正在等待生成订单。

对于这两种状态,我们需要用redis来实现,思路是:在系统初始化的时候,redis中设置秒杀商品是否卖完的状态为false—即未卖完;

public long getMiaoshaResult(Long userId, long goodsId) {
    MiaoshaOrder orderInfo = orderService.getMiaoshaOrderByUserIdGoodsId(userId,goodsId);
    if(orderInfo != null){
        return orderInfo.getId();
    }else{
        boolean isOver = getGoodsOver(goodsId);
        if(isOver){
            //库存已经没了
            return -1;
        }else{
            //表示还没入库,继续等待结果
            return 0;
        }
    }
}

在MiaoshaService中的Miaosha方法:数据库减库存失败的话,说明数据库的库存已经小于0了,那么这个时候,立即将redis初始设置的秒杀商品是否卖完的状态为true,表示商品已经全部卖完,返回秒杀失败。否则就是要前端等待等待。
原来的代码:

	@Transactional
	public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
		//减库存 下订单 写入秒杀订单
		goodsService.reduceStock(goods);
		//order_info maiosha_order
		return orderService.createOrder(user, goods);
	}

改为:

@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
    //减库存、下订单、写入秒杀订单
    boolean success =goodsService.reduceStock(goods);
    if(success){
        return orderService.createOrder(user,goods);
    }else{
        setGoodsOver(goods.getId());
        return null;
    }
}

对于两个小方法getGoodsOver和setGoodsOver:

private void setGoodsOver(long goodId){
    redisService.set(MiaoshaKey.isGoodsOver,""+goodId,true);
}

private boolean getGoodsOver(long goodsId) {
    return redisService.exists(MiaoshaKey.isGoodsOver,""+goodsId);
}

那么redis预减库存,然后消息队列来进行创建订单就实现了。

当然,对于redis预减库存这一点,还有要优化的地方,就是现在的do_miaosha接口是这样的:

@RequestMapping(value = "/do_miaosha",method = RequestMethod.POST)
@ResponseBody
public Result<Integer> do_miaosha(Model model, MiaoshaUser user, @RequestParam("goodsId") long goodsId){
    if(user == null)
        return Result.error(CodeMsg.SESSION_ERROR);

    //1.预减库存进行优化
    /*********************************优化1开始*************************************/
    long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId);
    if(stock < 0){
        return Result.error(CodeMsg.MIAO_SHA_OVER);
    }
    /*********************************优化1结束*************************************/
    
    //2.判断是否已经秒杀到了
    MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(),goodsId);
    if(miaoshaOrder != null){
        return Result.error(CodeMsg.REPEATE_MIAOSHA);
    }
    /*********************************优化2开始*************************************/
    //3.进入消息队列
    MiaoshaMessage message = new MiaoshaMessage();
    message.setUser(user);
    message.setGoodsId(goodsId);
    sender.sendMiaoshaMessage(message);
    /*********************************优化2结束*************************************/
    return Result.success(0);//排队中
}

但是,当秒杀商品已经没了的时候,就没有必要再去redis中进行判断了,毕竟查询redis也是需要网络开销的,解决思路是:在内存中进行判断,如果redisService.decr得到的stock少于零的时候,直接将内存中的一个标志改变一下,那么下次再进入do_miaosha接口,先判断内存这个标记,如果库存已经小于0了,就不再访问redis,而是直接返回秒杀商品已经卖完。
改为:

@RequestMapping(value = "/do_miaosha",method = RequestMethod.POST)
@ResponseBody
public Result<Integer> do_miaosha(Model model, MiaoshaUser user, @RequestParam("goodsId") long goodsId){
    if(user == null)
        return Result.error(CodeMsg.SESSION_ERROR);

    //内存标记,减少不必要的redis的访问
    boolean over = localOverMap.get(goodsId);
    if(over){
        return Result.error(CodeMsg.MIAO_SHA_OVER);
    }

    //预减库存进行优化
    long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId);
    if(stock < 0){
        localOverMap.put(goodsId,true);
        return Result.error(CodeMsg.MIAO_SHA_OVER);
    }
    //判断是否已经秒杀到了
    MiaoshaOrder miaoshaOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(),goodsId);
    if(miaoshaOrder != null){
        return Result.error(CodeMsg.REPEATE_MIAOSHA);
    }
    //进入消息队列
    MiaoshaMessage message = new MiaoshaMessage();
    message.setUser(user);
    message.setGoodsId(goodsId);
    sender.sendMiaoshaMessage(message);

    return Result.success(0);//排队中
}

声明一个map:

private Map<Long,Boolean> localOverMap = new HashMap<>();

那么在afterPropertiesSet这个系统加载的初始化方法中对这个map进行初始化,goodsId–stock:

localOverMap.put(goods.getId(),false);

在原来的redis预减库存初,发现库存小于0 ,就改为true:

if(stock < 0){
    localOverMap.put(goodsId,true);
    return Result.error(CodeMsg.MIAO_SHA_OVER);
}

这样,整个关于redis预减库存和rabbitMQ创建订单这个优化已经基本完成了。

部分摘自:http://fossi.oursnail.cn/2019/04/23/miaosha/6.服务级高并发秒杀优化(RabbitMQ+接口优化)/

猜你喜欢

转载自blog.csdn.net/NIUBILISI/article/details/89884007
今日推荐