rabbitMQ
消息队列,通过一定的通信协议,生产者和消费者在应用程序内传递通信。
主要的作用,提高负载,减耦合。
场景描述:当点击秒杀按钮的那个时刻,有很高的并发量,客户端发出请求之后,会判断库存,如果库存大于0,就判断是否已经下单,如果没有下单,就执行秒杀逻辑,对于秒杀逻辑,分两个步骤,一是减库存,二是创建订单。
以上就是不使用rabbitMQ的场景描述。
利用消息队列,我们可以在执行秒杀逻辑之前,将用户和待秒杀的商品进行入队(rabbitMQ的生产者类),然后将秒杀逻辑放在出队上(rabbitMQ的消费者类)
在客户端页面上创建一个轮询函数,每隔一段时间,设置一个请求是否有秒杀结果,然后进行数据展示,或者跳转。
通过以上操作,我们可以将用户的请求与返回的结果通过rabbitMQ进行解耦合。从而降低网站的并发。
-
内存标记库存
假如后面的操作已经确认库存小于0,则设置一个标记器,直接返回错误信息。
//内存标记,第一次拦截 Boolean over = localOverMap.get(goodsId); if(over){ return Result.error(CodeMsg.MIAO_SHA_OVER); }
-
预减库存
该操作相当于一个拦截器,当我们的秒杀请求进到秒杀控制器中,需要先从redis中减库存,然后判断库存是否充足。因此,需要先在redis中加载商品的库存数
这里利用控制器继承一个接口,来写一个系统初始化的方法,将商品的库存数放到redis中,每次从秒杀点击过来的请求,先加载该初始化方法,把库存数从数据库里拿出来,(这里可以改进为从缓存中拿值),然后判断库存数,如果库存数小于0,则不需要后续操作,直接返回错误。
/** * 系统初始化 * @throws Exception */ @Override 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()); //对内存进行标志,若没有结束库存就是false,若有库存就是true localOverMap.put(goods.getId(),false);//false表示有库存 } } //预减库存 Long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId); if(stock<0){ localOverMap.put(goodsId,true); return Result.error(CodeMsg.MIAO_SHA_OVER); }
当判断,既有库存,又没有下订单,则执行入队操作,将需要传递的用户和商品名加入到队列中,传递给消费者。
//判断是否已经秒杀到了 MiaoshaOrder order=orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId); if(order!=null){ return Result.error(CodeMsg.REPEATE_MIAOSHA); } //入队操作 //创建需要入队的数据类,需要用户类和商品的id。 MiaoshaMessage message=new MiaoshaMessage(); message.setGoodsId(goodsId); message.setUser(user); //调用rabbitMQ生产者的发送方法,发送该数据类 mqSender.sendMiaoshaMessage(message);
-
完整MiaoshaController类
@Controller @RequestMapping("/miaosha") public class MiaoshaController implements InitializingBean{ @Autowired private GoodsService goodsService; @Autowired private OrderService orderService; @Autowired private MiaoshaService miaoshaService; @Autowired private RedisService redisService; @Autowired private MQSender mqSender; private HashMap<Long,Boolean> localOverMap=new HashMap<>(); /** * 系统初始化 * @throws Exception */ @Override 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()); //对内存进行标志,若没有结束库存就是false,若有库存就是true localOverMap.put(goods.getId(),false);//false表示有库存 } } @RequestMapping(value = "/do_miaosha",method = RequestMethod.POST) @ResponseBody public Result<Integer> list(Model model, MiaoshaUser user, @RequestParam("goodsId") long goodsId){ model.addAttribute("user",user); if(user==null){ //如果没有获取到user值,报异常 return Result.error(CodeMsg.SESSION_ERROR); } //内存标记,第一次拦截 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 order=orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId); if(order!=null){ return Result.error(CodeMsg.REPEATE_MIAOSHA); } //入队操作 //创建需要入队的数据类,需要用户类和商品的id。 MiaoshaMessage message=new MiaoshaMessage(); message.setGoodsId(goodsId); message.setUser(user); //调用rabbitMQ生产者的发送方法,发送该数据类 mqSender.sendMiaoshaMessage(message); return Result.success(0);//返回0代表排队中 // //判断库存 // GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId); // Integer stock= goods.getStockCount(); // // if(stock<=0){ // model.addAttribute("errmsg", CodeMsg.MIAO_SHA_OVER.getMsg()); // return Result.error(CodeMsg.MIAO_SHA_OVER); // } // // //判断是否已经秒杀到了 // MiaoshaOrder order=orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId); // // if(order!=null){ // return Result.error(CodeMsg.REPEATE_MIAOSHA); // } // // //进行秒杀逻辑 // //减库存,下订单,写入秒杀订单 // OrderInfo orderInfo=miaoshaService.miaosha(user, goods); // return Result.success(orderInfo); } @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); } }
将队列的消息寄出去之后,需要由消费者接收数据,并执行秒杀的具体逻辑
这里的逻辑代码就很简单,继续判断库存以及是否秒杀到,然后执行秒杀逻辑
- MQReceiver类
@Service public class MQReceiver { private static Logger log = LoggerFactory.getLogger(MQReceiver.class); @Autowired private GoodsService goodsService; @Autowired private OrderService orderService; @Autowired private MiaoshaService miaoshaService; @RabbitListener(queues= MQConfig.MIAOSHA_QUEUE) public void receive(String message){ //获取到生产者发送过来的数据 log.info("收到的消息:"+message); //转换数据格式 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); } }
具体的秒杀逻辑是减库存,然后创建订单,这些代码在之前已经写过。如果没有减库存成功,则表示已经结束秒杀了,在之前的代码中,什么都没做,让事务进行回滚。这次在缓存中设置一个秒杀结束的标志。
@Transactional public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) { // //减库存,下订单,写入秒杀订单 // goodsService.reduceStock(goods); // return orderService.createOrder(user, goods); boolean success = goodsService.reduceStock(goods); if (success){ return orderService.createOrder(user, goods); }else { setGoodsOver(goods.getId()); return null; } } private void setGoodsOver(Long goodsId) { redisService.set(MiaoshaKey.isGoodsOver, ""+goodsId, true); } private boolean getGoodsOver(long goodsId) { return redisService.exists(MiaoshaKey.isGoodsOver, ""+goodsId); }
做到此处,秒杀的逻辑结束了,但并没有返回任何页面,只是对数据库做了相应的修改。
接下来应该在客户端进行循环提出请求,来判断是否秒杀成功,在商品的详情页面,当入队成功返回一个0时,就开始启动轮询函数,不停的向服务器发请求,看是否已经下单成功,若成功就跳转到订单详情页面。
function getMiaoshaResult(goodsId) { g_showLoading();//显示处理等待 //发起ajax请求 $.ajax({ url:"/miaosha/result", type:"GET", data:{ goodsId:$("#goodsId").val() }, success:function(data){ if (data.code==0){ var result=data.data; if(result<0){ layer.msg("对不起,秒杀失败"); }else if(result==0){ //继续轮询 setTimeout(function () { getMiaoshaResult(goodsId) },50); } else { layer.confirm("恭喜你,秒杀成功!查看订单?", {btn:["确定","取消"]}, function(){ window.location.href="/order_detail.htm?orderId="+result; }, function(){ layer.closeAll(); }); } }else { layer.msg(data.msg); } }, error:function(){ layer.msg("客户端请求有误"); } }); }
每次轮询代码请求服务器时,都会将秒杀的结果返回
public long getMiaoshaResult(Long userId, long goodsId) { //查看是否下订单成功 MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(userId, goodsId); if (order!=null){ return order.getOrderId(); } else { boolean isover = getGoodsOver(goodsId); if(isover){ return -1; } else { return 0; } } }
若订单不为空,则返回订单的id,若为空,则查看有没有库存,若没有库存则返回-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); }
到此,rabbitMQ实现的秒杀功能就完成了。