基于springboot+redis+rabbitmq的高并发秒杀系统实现-4

项目源码下载地址:

https://github.com/wangqianlong513/springboot-redis-rabbitmq-seckill

上篇讲述到了商品详情goods_detail.html页的展示问题,此篇开始讲述秒杀过程

1、点击详情页中的“秒杀”按钮,会触发onclick事件getMiaosshaPath()。

<button class="btn btn-primary" type="button" id="buyButton"onclick="getMiaoshaPath()">立即秒杀</button>

2、getMiaoshaPath()方法如下。本秒杀系统在设计的时候,为了安全问题,隐藏了秒杀地址(防止恶刷)。每个人的秒杀地址是不同的,而且每个人的秒杀地址是在点击“秒杀”按钮的时候动态生成的。所谓动态生成无非就是在秒杀地址的url中拼接一个动态生成的参数。下面就具体讲述秒杀地址的隐藏问题。点击“秒杀”按钮时,在getMiaoshaPath方法中会执行ajax请求,请求的后台地址是/miaosha/path。向后台传递的参数包括goodsId和前端输入的验证码(要在验证码验证通过后才动态生成path参数)

function getMiaoshaPath(){
	var goodsId = $("#goodsId").val();
	g_showLoading();
	$.ajax({
		url:"/miaosha/path",
		type:"GET",
		data:{
			goodsId:goodsId,
			verifyCode:$("#verifyCode").val()
		},
		success:function(data){
			if(data.code == 0){
				var path = data.data;
				doMiaosha(path);
			}else{
				layer.msg(data.msg);
			}
		},
		error:function(){
			layer.msg("客户端请求有误");
		}
	});
}

3、后台的/miaosha/path方法如下。此方法也专门定义了一个@AccessLimit注解来限制接口的在单位时间的访问次数,属于限流的措施。后面再进行讲解。此处先讲解秒杀路径和验证码后台验证的问题。可以看到getMiaoshaPath接收了goodsId和verrfyCode参数。调用了miaoshaService的checkVerifyCode(验证码在保存到redis的时候,key中含有userId和goodId,此处就是按照userId和goodId来获取redis中正确的验证码)方法来验证前端输入的验证码是否正确。同时调用了miaoshaService中的crateMiaoshaPath方法来生成秒杀路径(注意,path路径的生成要依赖于当前的user和商品id)

 @AccessLimit(seconds=5, maxCount=5, needLogin=true)
    @RequestMapping(value="/path", method=RequestMethod.GET)
    @ResponseBody
    public Result<String> getMiaoshaPath(HttpServletRequest request, MiaoshaUser user,
    		@RequestParam("goodsId")long goodsId,
    		@RequestParam(value="verifyCode", defaultValue="0")int verifyCode
    		) {
    	if(user == null) {
    		return Result.error(CodeMsg.SESSION_ERROR);
    	}
    	boolean check = miaoshaService.checkVerifyCode(user, goodsId, verifyCode);
    	if(!check) {
    		return Result.error(CodeMsg.REQUEST_ILLEGAL);
    	}
    	String path  =miaoshaService.createMiaoshaPath(user, goodsId);
    	return Result.success(path);
    }

4、miaoshaService的checkVerifyCode方法如下。在上一篇讲述生成验证码的时候,讲到生成的验证码的计算结果保存到了redis中,此处验证的时候就是从redis中获取后台计算的结果。然后与用户输入的验证码进行匹配,相等的话则说明用户输入的验证码就是正确的,可以继续往下执行生成秒杀路径的逻辑。

public boolean checkVerifyCode(MiaoshaUser user, long goodsId, int verifyCode) {
		if(user == null || goodsId <=0) {
			return false;
		}
		Integer codeOld = redisService.get(MiaoshaKey.getMiaoshaVerifyCode, user.getId()+","+goodsId, Integer.class);
		if(codeOld == null || codeOld - verifyCode != 0 ) {
			return false;
		}
		redisService.delete(MiaoshaKey.getMiaoshaVerifyCode, user.getId()+","+goodsId);
		return true;
	}

5、生成秒杀路径的逻辑如下。也就是使用UUID生成随机数,然后进行一次md5加密,最终生成一个随机字符串。把这个生成的随机字符串保存如redis中(注意:保存到redis的时候,key中仍然包含了userId和goodId)

public String createMiaoshaPath(MiaoshaUser user, long goodsId) {
	if(user == null || goodsId <=0) {
		return null;
	}
	String str = MD5Util.md5(UUIDUtil.uuid()+"123456");
    redisService.set(MiaoshaKey.getMiaoshaPath, ""+user.getId() + "_"+ goodsId, str);
	return str;
}

6、上述4和5执行后,代码执行到了3中的return Result.success(path)语句。进入返回到2中getMiaoshaPath方法中的success代码块中。success代码块中要执行js方法doMiaosha(path),此方法如下。可以看到,在doMiaosha方法中,请求的后台方法中拼接了上面生成的path,也即整个路径是临时拼接出来的,不是固定的。同时向这个后台方法中传入参数goodsId。

function doMiaosha(path){
	$.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("客户端请求有误");
		}
	});
	
}

7、后台的/miaosha/"+path+"/do_miaosha方法如下。此方法中,接收了path和goodsId,接收到了path后,调用checkPath方法来判断此path是否正确,验证方法见下面的8。正确的话再继续往下执行。需要说明的是,在本系统启动的时候,会进行系统初始化操作:把数据库中所有秒杀商品查询出来,然后把商品id和对应的数量存入到redis中,后续秒杀操作,直接针对redis进行库存充足判断以及库存加减操作。初始化方法见下面的8。所以在此方法中,先使用redis的decr操作预减库存,因为redis单线程模型,所以保证同一个时刻,只有一个线程能够执行库存减一操作。decr返回值stock是减1后的结果,再判断此stock是否大于0。大于0说明还有库存,小于0说明没有库存,修改状态值localOverMap,这个是一个标记值,为true表示没有库存了。后续秒杀操作也就不会执行redis操作了,作用是减少redis操作次数。然后判断是否之前已经秒杀过此商品,所以需要查询一下订单,见下面10,查看此用户是否存在抢购此商品的订单,如果存在则说明已经购买过,就不允许购买了。如果不存在,则说明此用户之前没有抢到过此商品,所以可以继续往下执行操作。往下的操作就是用到了rabbitmq的异步生成订单的操作。需要把goodsId和userId传递到队列中。但是给前端用户返回的内容是Result.success(0),然后程序执行到上述js方法中的success模块。进而执行js方法getMiaoshaResult方法。见下面的11。

总结一下秒杀逻辑。

(1)先判断秒杀路径是否正确,验证path的合法性。

(2)path合法时,判断秒杀完毕标记变量localOverMap的值,为true表示没有库存了,直接返回error。为false则继续执行

(3)预减库存,也即让redis中的商品库存减一,并判断减一后的库存与0的关系。为正,则说明库存充足;为负,则说明库存不足,同时修改localOverMap标记。

(4)当上述库存满足的时候,未必就能完成此次秒杀,还要判断之前是否已经秒杀到,防止重复秒杀。

(5)当之前没有秒杀到商品时,可以进行秒杀。

@RequestMapping(value="/{path}/do_miaosha", method=RequestMethod.POST)
    @ResponseBody
    public Result<Integer> miaosha(Model model,MiaoshaUser user,
    		@RequestParam("goodsId")long goodsId,
    		@PathVariable("path") String path) {
    	model.addAttribute("user", user);
    	if(user == null) {
    		return Result.error(CodeMsg.SESSION_ERROR);
    	}
    	//验证path
    	boolean check = miaoshaService.checkPath(user, goodsId, path);
    	if(!check){
    		return Result.error(CodeMsg.REQUEST_ILLEGAL);
    	}
    	//内存标记,减少redis访问
    	boolean over = localOverMap.get(goodsId);
    	if(over) {
    		return Result.error(CodeMsg.MIAO_SHA_OVER);
    	}
    	//预减库存
    	long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//10
    	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);
    	}
    	//入队
    	MiaoshaMessage mm = new MiaoshaMessage();
    	mm.setUser(user);
    	mm.setGoodsId(goodsId);
    	sender.sendMiaoshaMessage(mm);
    	return Result.success(0);//排队中
    }

 8、验证秒杀路径是否正确

public boolean checkPath(MiaoshaUser user, long goodsId, String path) {
		if(user == null || path == null) {
			return false;
		}
		String pathOld = redisService.get(MiaoshaKey.getMiaoshaPath, ""+user.getId() + "_"+ goodsId, String.class);
		return path.equals(pathOld);
	}

9、系统初始化,把数据库中库存数量保存到redis中

	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);
		}
	}

10、getMiaoshaOrderByUserIdGoodsId是根据userId和goodsId来查询订单

public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) {
		return redisService.get(OrderKey.getMiaoshaOrderByUidGid, ""+userId+"_"+goodsId, MiaoshaOrder.class);
	}

11、getMiaoshaResult方法,此方法是每隔0.2秒轮询查询是否存在已经生成了订单。

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;
				if(result < 0){
					layer.msg("对不起,秒杀失败");
				}else if(result == 0){//继续轮询
					setTimeout(function(){
						getMiaoshaResult(goodsId);
					}, 200);
				}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("客户端请求有误");
		}
	});
}

 

至此,秒杀操作已经完成,秒杀商品的igoodsId和用户的userId也已经通过sender.sendMiaoshaMessage(mm)操作开始往rabbitmq队列中发送消息。下面一篇将介绍异步生成订单、发送邮件、短信通知的功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值