【SpringBoot商城秒杀系统项目实战21】高并发秒杀系统接口优化 RabbitMQ异步下单
2019-05-30 16:44
976 查看
【秒杀系统的接口优化之异步下单】
问题:
针对秒杀的业务场景,在大并发下,仅仅依靠页面缓存、对象缓存或者页面静态化等还是远远不够。数据库压力还是很大,所以需要异步下单,如果业务执行时间比较长,那么异步是最好的解决办法,但会带来一些额外的程序上的复杂性。
思路:
- 系统初始化,把商品库存数量stock加载到Redis上面来。
- 后端收到秒杀请求,Redis预减库存,如果库存已经到达临界值的时候,就不需要继续请求下去,直接返回失败,即后面的大量请求无需给系统带来压力。
- 判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品,判断是否重复秒杀。
- 库存充足,且无重复秒杀,将秒杀请求封装后消息入队,同时给前端返回一个code (0),即代表返回排队中。(返回的并不是失败或者成功,此时还不能判断)
- 前端接收到数据后,显示排队中,并根据商品id轮询请求服务器(考虑200ms轮询一次)。
- 后端RabbitMQ监听秒杀MIAOSHA_QUEUE的这名字的通道,如果有消息过来,获取到传入的信息,执行真正的秒杀之前,要判断数据库的库存,判断是否重复秒杀,然后执行秒杀事务(秒杀事务是一个原子操作:库存减1,下订单,写入秒杀订单)。
- 此时,前端根据商品id轮询请求接口MiaoshaResult,查看是否生成了商品订单,如果请求返回-1代表秒杀失败,返回0代表排队中,返回>0代表商品id说明秒杀成功。
返回结果说明:
前端会根据后端返回的值来判断是秒杀结果。
-1 :库存不足秒杀失败
0 :排队中,继续轮询
>0 :返回的是商品id ,说明秒杀成功
1.后端接收秒杀请求的接口doMiaosha。
@RequestMapping(value="/{path}/do_miaosha",method=RequestMethod.POST) @ResponseBody public Result<Integer> doMiaosha(Model model,MiaoshaUser user, @RequestParam(value="goodsId",defaultValue="0") long goodsId, @PathVariable("path")String path) { model.addAttribute("user", user); //1.如果用户为空,则返回至登录页面 if(user==null){ return Result.error(CodeMsg.SESSION_ERROR); } //2.预减少库存,减少redis里面的库存 long stock=redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId); //3.判断减少数量1之后的stock,区别于查数据库时候的stock<=0 if(stock<0) {//线程不安全---库存至临界值1的时候,此时刚好来了加入10个线程,那么库存就会-10 return Result.error(CodeMsg.MIAOSHA_OVER_ERROR); } //4.判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品 MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdAndCoodsId(user.getId(), goodsId); if (order != null) {// 查询到了已经有秒杀订单,代表重复下单 return Result.error(CodeMsg.REPEATE_MIAOSHA); } //5.正常请求,入队,发送一个秒杀message到队列里面去,入队之后客户端应该进行轮询。 MiaoshaMessage mms=new MiaoshaMessage(); mms.setUser(user); mms.setGoodsId(goodsId); mQSender.sendMiaoshaMessage(mms); //返回0代表排队中 return Result.success(0); } //MiaoshaMessage 消息的封装 MiaoshaMessage Bean public class MiaoshaMessage { private MiaoshaUser user; private long goodsId; public MiaoshaUser getUser() { return user; } public void setUser(MiaoshaUser user) { this.user = user; } public long getGoodsId() { return goodsId; } public void setGoodsId(long goodsId) { this.goodsId = goodsId; } }
注意:消息队列这里,消息只能传字符串,MiaoshaMessage 这里是个Bean对象,是先用beanToString方法,将转换为String,放入队列,使用AmqpTemplate传输。
@Autowired RedisService redisService; @Autowired AmqpTemplate amqpTemplate; public void sendMiaoshaMessage(MiaoshaMessage mmessage) { // 将对象转换为字符串 String msg = RedisService.beanToString(mmessage); log.info("send message:" + msg); // 第一个参数队列的名字,第二个参数发出的信息 amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg); } /** * 将Bean对象转换为字符串类型 * @param <T> */ public static <T> String beanToString(T value) { //如果是null if(value==null) return null; //如果不是null Class<?> clazz=value.getClass(); if(clazz==int.class||clazz==Integer.class) { return ""+value; }else if(clazz==String.class) { return ""+value; }else if(clazz==long.class||clazz==Long.class) { return ""+value; }else { return JSON.toJSONString(value); } }
2.监控该消息队列,一旦有消息进入,从该消息中获取对象进行秒杀操作
@RabbitListener(queues=MQConfig.MIAOSHA_QUEUE)//指明监听的是哪一个queue public void receiveMiaosha(String message) { log.info("receiveMiaosha message:"+message); //通过string类型的message还原成bean,拿到了秒杀信息之后。开始业务逻辑秒杀, MiaoshaMessage mm=RedisService.stringToBean(message, MiaoshaMessage.class); MiaoshaUser user=mm.getUser(); long goodsId=mm.getGoodsId(); GoodsVo goodsvo=goodsService.getGoodsVoByGoodsId(goodsId); int stockcount=goodsvo.getStockCount(); //1.判断库存不足 if(stockcount<=0) { return; } //2.判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品 MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdAndCoodsId(user.getId(), goodsId); if (order != null) {// 重复下单 return; } //原子操作:1.库存减1,2.下订单,3.写入秒杀订单--->是一个事务 miaoshaService.miaosha(user,goodsvo); } @Transactional public OrderInfo miaosha(MiaoshaUser user, GoodsVo goodsvo) { //1.减少库存,即更新库存 boolean success=goodsService.reduceStock1(goodsvo);//考虑减少库存失败的时,不进行写入订单 if(success) { //2.下订单,其中有两个订单: order_info miaosha_order OrderInfo orderinfo=orderService.createOrder_Cache(user, goodsvo); return orderinfo; }else {//减少库存失败,做一个标记,代表商品已经秒杀完了。 setGoodsOver(goodsvo.getId()); return null; } } //写入缓存 private void setGoodsOver(Long goodsId) { redisService.set(MiaoshaKey.isGoodsOver, ""+goodsId, true); } //查看缓存中是否有该key private boolean getGoodsOver(Long goodsId) { return redisService.exitsKey(MiaoshaKey.isGoodsOver, ""+goodsId); }
注意:秒杀操作是一个事务,使用@Transactional注解来标识,如果减少库存失败,则回滚。
3.前端根据商品id轮询请求接口MiaoshaResult,查看是否生成了商品订单,后端处理秒杀逻辑,并向前端返回请求结果。
/** * 客户端做一个轮询,查看是否成功与失败,失败了则不用继续轮询。 * 秒杀成功,返回订单的Id。 * 库存不足直接返回-1。 * 排队中则返回0。 * 查看是否生成秒杀订单。 */ @RequestMapping(value = "/result", method = RequestMethod.GET) @ResponseBody public Result<Long> doMiaoshaResult(Model model, MiaoshaUser user, @RequestParam(value = "goodsId", defaultValue = "0") long goodsId) { long result=miaoshaService.getMiaoshaResult(user.getId(),goodsId); System.out.println("轮询 result:"+result); return Result.success(result); } public long getMiaoshaResult(Long userId, long goodsId) { //先去缓存里面取得 MiaoshaOrder order=orderService.getMiaoshaOrderByUserIdAndGoodsId(userId, goodsId); //秒杀成功 if(order!=null) { return order.getOrderId(); } else { //查看商品是否卖完了 boolean isOver=getGoodsOver(goodsId); if(isOver) {//商品卖完了 return -1; }else { //商品没有卖完 return 0; } } }
注意:然后轮询访问 doMiaoshaResult这个接口,从数据库中拿订单,如果有。返回商品id,说明秒杀成功,通过从redis中拿到isOver标记来判断失败还是在请求,商品卖完了返回-1,商品没有卖完返回0,继续请求,前端拿到返回的数据,通过判断,进行显示,成功就跳转订单页面。
前端轮询业务代码:
function doMiaosha(path) { alert(path); alert("秒杀!"); $.ajax({ url : "/miaosha/" + path + "/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; //轮询 getMiaoshaResult($("#goodsId").val()); } else { layer.msg(data.msg); } }, error : function() { layer.msg("请求有误!"); } }); } //做轮询 function getMiaoshaResult(goodsId) { $.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); }, 200);//200ms之后继续轮询 layer.msg(data.msg); } 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("请求有误!"); } }); }
注意setTimeout的用法。
setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式。
1000 毫秒= 1 秒。
如果你只想重复执行可以使用 setInterval() 方法。
使用 clearTimeout() 方法来阻止函数的执行。
相关文章推荐
- 【SpringBoot商城秒杀系统项目实战24】安全优化 接口限流防刷
- 【SpringBoot商城秒杀系统项目实战22】安全优化 秒杀接口地址隐藏
- 【SpringBoot商城秒杀系统项目实战23】安全优化 数学图形验证码
- java架构师、集群、高可扩展、高性能、高并发、性能优化、Spring boot、Dubbo、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分布式项目实战学习架构师之路
- 【SpringBoot商城秒杀系统项目总结】 项目的亮点和难点及问题解决(源码地址)
- 2019Java秒杀系统方案优化 高性能高并发项目实战
- Java秒杀系统方案优化视频教程 Java高性能高并发实战教程
- 2018最全Java秒杀系统方案优化 高性能高并发实战教程
- Java秒杀系统方案优化 高性能高并发实战视频
- Java秒杀系统方案优化 高性能高并发实战
- Java秒杀系统方案优化 高性能高并发实战教程 2018最新
- 商城项目实战34:单点登录系统SSO最小实现的接口文档及Cookie工具类
- SpringBoot/Netty+MUI打造聊天系统项目实战(完整)
- 2018年最全Java秒杀系统方案优化 高性能高并发实战教程
- 2019新版《Java秒杀系统方案优化高性能高并发实战教程》
- 最新《Java秒杀系统方案优化》 高性能高并发实战教程
- STS创建Spring Boot项目实战(Rest接口、数据库、用户认证、分布式Token JWT、Redis操作、日志和统一异常处理)
- SSM项目实战(一)--- 高并发秒杀系统之DAO层
- 优效学院 基于微服务的秒杀项目实战 Spring Boot 2.0基础篇01
- Springboot+mongo 商城秒杀系统