秒杀页面展示:
第一次点击秒杀,减库存下订单,再次点击造成重复秒杀,无法购买。
秒杀逻辑:
①为秒杀按钮绑定点击事件获取秒杀地址----------οnclick="getSeckillPath()",当点击秒杀按钮,通过ajax异步提交获取秒杀地址;
//获取秒杀地址
function getSeckillPath(){
var goodsId = $("#goodsId").val();
$.ajax({
url:"/seckill_ssm/"+goodsId+"/getPath",
type:"GET",
success:function(result){
if (result.code == 0){
//获取秒杀path
var path = result.data;
domiaosha(path);//在服务端对path验证成功后才执行秒杀
}else{
layer.msg(result.msg);
}
},
error:function(){
layer.msg("请求不合法");
}
});
}
具体步骤为:
根据路径参数找到对应的Controller获取秒杀Path返回,秒杀Path其实就是一个随机串,主要是对随机生成的UUID做一次MD5计算然后将Path写入redis缓存中。如果ajax请求返回的信息码是0,表明秒杀Path成功,否则提示用户错误信息。
@RequestMapping("/{goodsId}/getPath")
@ResponseBody
public Result<String> getMiaoShaPath(Model model,
@CookieValue(value=UserServiceImpl.COOKI_NAME_TOKEN, required=false) String
cookieToken, HttpServletResponse response,@PathVariable("goodsId")long goodsId){
User user = userService.getByToken(response, cookieToken);
//如果用户为空,不能进行秒杀,返回登录界面
if (user == null){
return Result.error(CodeMsg.NAME_EMPTY);
}
//生成随机串秒杀Path
String path = seckillService.createSeckillPath(user,goodsId);
log.info("path:"+path);
return Result.success(path);
}
秒杀Path获取函数
public String createSeckillPath(User user, long goodsId) {
String path = Md5Util.md5(UUIDutil.uuid()+"123456");
redisService.set(MiaoshaKey.getSeckillPath,""+user.getId()+"_"+goodsId, path);
return path;
}
②将秒杀Path提交给服务端进行验证,验证通过后执行真正的秒杀操作。
function domiaosha(path){
$.ajax({
url:"/seckill_ssm/"+path+"/do_seckill",
type:"POST",
data:{goodsId:$("#goodsId").val()},
success:function(result){
if (result.code ==0){
getMiaoShaResult($("#goodsId").val());
}else{
//显示信息
layer.msg(result.msg);
}
}
});
}
③秒杀业务逻辑
a.首先从redis中预减对应商品的库存,返回的是剩下的库存stock,随后对stock进行一个判断,如果stock<0,则表示库存不足,直接返回库存不足的信息。
b.查询数据库中是否已经有对应的用户id和商品id的秒杀单形成,如果有则表明此次秒杀为重复下单,否则将此次秒杀的用户信息和商品id封装起来发送到队列中。
c.队列消费者监听队列,有任务后开始消费(处理)。从队列中拿到之前封装的对象,开始进行减库存、下订单操作。
d.首先仍然是判断库存是否充足和是否重复下单行为。其后进行减库存、下订单的原子操作,需要加上事务,以便在其中一项操作未能成功时进行事务回滚。如果库存更新成功,则创建订单,返回订单,同时将订单存入redis缓存中。
e.与此同时,客户端对返回的信息进行处理,分为秒杀失败、排队中(消息队列正在处理秒杀业务)、订单生成成功。对于排队中客户端需要进行轮询,直到秒杀订单形成。
function getMiaoShaResult(goodsId){
$.ajax({
url:"/seckill_ssm/getResult",
type:"GET",
data:{goodsId:$("#goodsId").val()},
success:function(result){
if (result.code == 0){
var data = result.data;
if (data < 0){
layer.msg("抱歉,秒杀失败");
}else if(data == 0){
//继续轮询
setTimeout(function(){
getMiaoShaResult(goodsId);
},50);//50ms轮询一次
//layer.msg(result.msg);
}else{
layer.confirm("恭喜你,秒杀成功!查看订单?",{btn:["确定","取消"]},
function(){
//确定跳转订单详情页面
alert(goodsId);
window.location.href="/seckill_ssm/order_detail?
goodsId="+goodsId;
},
function(){
layer.closeAll();
}
);
}
}
},
error:function(){
layer.msg("请求有误!");
}
});
}
@RequestMapping("/{path}/do_seckill")
@ResponseBody
public Result<Integer> creatOrderDetail(Model model,
@CookieValue(value=UserServiceImpl.COOKI_NAME_TOKEN,required=false)String
cookieToken, HttpServletResponse response,@RequestParam("goodsId")long
goodsId,@PathVariable("path") String path){
//1根据token从缓存中取用户登录信息
User user = userService.getByToken(response, cookieToken);
//如果用户为空,不能进行秒杀,返回登录界面
if (user == null){
return Result.error(CodeMsg.NAME_EMPTY);//code 3; msg 用户名为空
}
//验证path地址
boolean isPathCorrect = seckillService.CheckPath(user.getId(),goodsId,path);
if (!isPathCorrect)
return Result.error(CodeMsg.REQUEST_ILLGEAL);
//2预减redis缓存中的库存(什么时候加到redis的缓存中的?)这里是减了之后剩余的库存
long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId);
System.out.println("stock:"+stock);//这里有个问题:同一用户可以重复减库存
//3.判断减少缓存中库存是否小于0
if (stock < 0){
return Result.error(CodeMsg.ORDER_ERROR);//库存不足
}
//4.秒杀单是否形成,避免重复秒杀
MiaoshaOrder order = orderService.getMiaoshaOrderById(user.getId(), goodsId);
if (order != null){
//重复下单
return Result.error(CodeMsg.ORDER_REPEATE);
}
//5.将请求入队列,
MiaoShaMessage mms = new MiaoShaMessage();
mms.setGoodsId(goodsId);
mms.setUser(user);
mQSender.sendMiaoShaMessage(mms);
return Result.success(0);
}