【SpringBoot商城秒杀系统项目实战21】高并发秒杀系统接口优化 RabbitMQ异步下单

【秒杀系统的接口优化之异步下单】

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

思路:

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

spring-boot-seckill分布式秒杀系统是一个用SpringBoot开发的从0到1构建的分布式秒杀系统,项目案例基本成型,逐步完善中。 开发环境: JDK1.8、Maven、Mysql、IntelliJ IDEA、SpringBoot1.5.10、zookeeper3.4.6、kafka_2.11、redis-2.8.4、curator-2.10.0 启动说明: 1、启动前 请配置application.properties中相关redis、zk以及kafka相关地址,建议在Linux下安装使用。 2、数据库脚本位于 src/main/resource/sql 下面,启动前请自行导入。 3、配置完成,运行Application中的main方法,访问 http://localhost:8080/seckill/swagger-ui.html 进行API测试。 4、秒杀商品页:http://localhost:8080/seckill/index.shtml ,部分功能待完成。 5、本测试案例单纯为了学习,某些案例并不适用于生产环境,大家根据所需自行调整。 秒杀架构: 架构层级 1、一般商家在做活动的时候,经常会遇到各种不怀好意的DDOS攻击(利用无辜的吃瓜群众夺取资源),导致真正的我们无法获得服务!所以说高防IP还是很有必要的。 2、搞活动就意味着人多,接入SLB,对多台云服务器进行流量分发,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 3、基于SLB价格以及灵活性考虑后面我们接入Nginx做限流分发,来保障后端服务的正常运行。 4、后端秒杀业务逻辑,基于Redis 或者 Zookeeper 分布式锁,Kafka 或者 Redis 做消息队列,DRDS数据库中间件实现数据的读写分离。 优化思路 1、分流、分流、分流,重要的事情说三遍,再牛逼的机器也抵挡不住高级别的并发。 2、限流、限流、限流,毕竟秒杀商品有限,防刷的前提下没有绝对的公平,根据每个服务的负载能力,设定流量极限。 3、缓存、缓存、缓存、尽量不要让大量请求穿透到DB层,活动开始前商品信息可以推送至分布式缓存。 4、异步异步异步,分析并识别出可以异步处理的逻辑,比如日志,缩短系统响应时间。 5、主备、主备、主备,如果有条件做好主备容灾方案也是非常有必要的(参考某年锤子的活动被攻击)。 6、最后,为了支撑更高的并发,追求更好的性能,可以对服务器的部署模型进行优化,部分请求走正常的秒杀流程,部分请求直接返回秒杀失败,缺点是开发部署时需要维护两套逻辑。 分层优化 1、前端优化:活动开始前生成静态商品页面推送缓存和CDN,静态文件(JS/CSS)请求推送至文件服务器和CDN。 2、网络优化:如果是全国用户,最好是BGP多线机房,减少网络延迟。 3、应用服务优化:Nginx最佳配置、Tomcat连接池优化、数据库配置优化、数据库连接池优化
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值