项目源码下载地址:
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队列中发送消息。下面一篇将介绍异步生成订单、发送邮件、短信通知的功能。