秒杀整体架构图
一、秒杀接口优化思路: 1、系统初始化把商品数量加载到redis 2、收到请求redis预减库存,库存不足直接返回,否则进入3 3、后端请求进入mq队列,前端显示请求中 4、请求出队,生成订单,减少库存 5、客户端轮询,是否秒杀成功 二、秒杀接口的隐藏:秒杀开始之前先去请求获取秒杀的地址 1、接口改造带上PathVariable参数 2、添加生成地址的接口 3、秒杀收到请求先验证PathVariable 三、图形验证码分散用户请求: 1、添加生成验证码的接口 2、在获取秒杀路径的时候,验证验证码 3、ScriptEngine的使用 四、接口防刷:用redis对接口限流
后端主要代码:
@Controller @RequestMapping("/miaosha") public class MiaoshaController implements InitializingBean { @Autowired MiaoshaUserService userService; @Autowired RedisService redisService; @Autowired GoodsService goodsService; @Autowired OrderService orderService; @Autowired MiaoshaService miaoshaService; @Autowired MQSender sender; private HashMap<Long, Boolean> localOverMap = new HashMap<Long, Boolean>(); /** * 系统初始化 * */ 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); } } @RequestMapping(value="/reset", method=RequestMethod.GET) @ResponseBody public Result<Boolean> reset(Model model) { List<GoodsVo> goodsList = goodsService.listGoodsVo(); for(GoodsVo goods : goodsList) { goods.setStockCount(10); redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), 10); localOverMap.put(goods.getId(), false); } redisService.delete(OrderKey.getMiaoshaOrderByUidGid); redisService.delete(MiaoshaKey.isGoodsOver); miaoshaService.reset(goodsList); return Result.success(true); } @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);//排队中 } /** * orderId:成功 * -1:秒杀失败 * 0: 排队中 * */ @RequestMapping(value="/result", method=RequestMethod.GET) @ResponseBody public Result<Long> miaoshaResult(Model model,MiaoshaUser user, @RequestParam("goodsId")long goodsId) { model.addAttribute("user", user); if(user == null) { return Result.error(CodeMsg.SESSION_ERROR); } long result =miaoshaService.getMiaoshaResult(user.getId(), goodsId); return Result.success(result); } @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); } @RequestMapping(value="/verifyCode", method=RequestMethod.GET) @ResponseBody public Result<String> getMiaoshaVerifyCod(HttpServletResponse response,MiaoshaUser user, @RequestParam("goodsId")long goodsId) { if(user == null) { return Result.error(CodeMsg.SESSION_ERROR); } try { BufferedImage image = miaoshaService.createVerifyCode(user, goodsId); OutputStream out = response.getOutputStream(); ImageIO.write(image, "JPEG", out); out.flush(); out.close(); return null; }catch(Exception e) { e.printStackTrace(); return Result.error(CodeMsg.MIAOSHA_FAIL); } } }
前端主要代码:
<div class="row"> <div class="form-inline"> <img id="verifyCodeImg" width="80" height="32" style="display:none" onclick="refreshVerifyCode()"/> <input id="verifyCode" class="form-control" style="display:none"/> <button class="btn btn-primary" type="button" id="buyButton"onclick="getMiaoshaPath()">立即秒杀</button> </div> </div>
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("客户端请求有误"); } }); 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("客户端请求有误"); } }); } 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("客户端请求有误"); } }); } function refreshVerifyCode(){ $("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val()+"×tamp="+new Date().getTime()); }
附录一:数据库的设计
商品表: create table Goods ( id bigint PRIMARY KEY , goodsName VARCHAR(255) , goodsTitle VARCHAR(255) , goodsImg VARCHAR(255) , goodsDetail VARCHAR(255) , goodsPrice double precision not null , goodsStock INTEGER )ENGINE =INNODB DEFAULT CHARSET= utf8; 秒杀商品表: create table MiaoshaGoods ( id bigint PRIMARY KEY , goodsId bigint , stockCount INTEGER , startDate null , endDate null )ENGINE =INNODB DEFAULT CHARSET= utf8; 秒杀订单表: create table MiaoshaOrder ( id bigint PRIMARY KEY , userId bigint , orderId bigint , goodsId bigint )ENGINE =INNODB DEFAULT CHARSET= utf8; 用户表: create table MiaoshaUser ( id bigint PRIMARY KEY , nickname VARCHAR(255) , password VARCHAR(255) , salt VARCHAR(255) , head VARCHAR(255) , registerDate null , lastLoginDate null , loginCount INTEGER )ENGINE =INNODB DEFAULT CHARSET= utf8; 订单表: create table OrderInfo ( id bigint PRIMARY KEY , userId bigint , goodsId bigint , deliveryAddrId bigint , goodsName VARCHAR(255) , goodsCount INTEGER , goodsPrice double precision not null , orderChannel INTEGER , status INTEGER , createDate null , payDate null )ENGINE =INNODB DEFAULT CHARSET= utf8;
附录二:拦截器内实现的接口限流
@Retention(RUNTIME) @Target(METHOD) public @interface AccessLimit { int seconds(); int maxCount(); boolean needLogin() default true; }
@Service public class AccessInterceptor extends HandlerInterceptorAdapter{ @Autowired MiaoshaUserService userService; @Autowired RedisService redisService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if(handler instanceof HandlerMethod) { MiaoshaUser user = getUser(request, response); UserContext.setUser(user); HandlerMethod hm = (HandlerMethod)handler; AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class); if(accessLimit == null) { return true; } int seconds = accessLimit.seconds(); int maxCount = accessLimit.maxCount(); boolean needLogin = accessLimit.needLogin(); String key = request.getRequestURI(); if(needLogin) { if(user == null) { render(response, CodeMsg.SESSION_ERROR); return false; } key += "_" + user.getId(); }else { //do nothing } AccessKey ak = AccessKey.withExpire(seconds); Integer count = redisService.get(ak, key, Integer.class); if(count == null) { redisService.set(ak, key, 1); }else if(count < maxCount) { redisService.incr(ak, key); }else { render(response, CodeMsg.ACCESS_LIMIT_REACHED); return false; } } return true; } private void render(HttpServletResponse response, CodeMsg cm)throws Exception { response.setContentType("application/json;charset=UTF-8"); OutputStream out = response.getOutputStream(); String str = JSON.toJSONString(Result.error(cm)); out.write(str.getBytes("UTF-8")); out.flush(); out.close(); } private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response) { String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN); String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN); if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) { return null; } String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken; return userService.getByToken(response, token); } private String getCookieValue(HttpServletRequest request, String cookiName) { Cookie[] cookies = request.getCookies(); if(cookies == null || cookies.length <= 0){ return null; } for(Cookie cookie : cookies) { if(cookie.getName().equals(cookiName)) { return cookie.getValue(); } } return null; } }
参考代码
https://gitee.com/lzhcode/maven-parent/tree/master/lzh-seckill2