本文是“Java秒杀系统实战系列文章”的第十八篇,我们将继续秒杀系统的优化之路。在本文中我们将基于 RabbitMQ 异步通信、FIFO(先进先出)、接口限流的特性,在执行秒杀核心的处理逻辑之前架上一层“限流”的处理逻辑,从而让瞬时产生的,犹如波涛汹涌、潮水般的请求流量变得井井有条、有序性地到达后端的秒杀接口!
接着上一篇章的讲解,我们需要在后端 接收前端高并发产生多线程请求时,及时高效地转移巨大的用户请求之MQ 中间件 中,为后端秒杀接口赢得足够的、规范化的处理!在这一过程,前端和后端的交互是异步的,因此,在前后端处理逻辑层面跟前面篇章的处理方式将有所不同。
(1)首先,在Controller层,需要提供响应前端秒杀请求的方法,该方法不直接处理秒杀的核心业务逻辑,而是将其转移至MQ中间件中,并立即返回success的状态信息给回到前端,其代码如下所示:
@Autowired
private RabbitSenderService rabbitSenderService;
//商品秒杀核心业务逻辑-mq限流
@RequestMapping(value = prefix+"/execute/mq",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public BaseResponse executeMq(@RequestBody @Validated KillDto dto, BindingResult result, HttpSession session){
if (result.hasErrors() || dto.getKillId()<=0){
return new BaseResponse(StatusCode.InvalidParams);
}
Object uId=session.getAttribute("uid");
if (uId==null){
return new BaseResponse(StatusCode.UserNotLogin);
}
Integer userId= (Integer)uId ;
BaseResponse response=new BaseResponse(StatusCode.Success);
Map<String,Object> dataMap= Maps.newHashMap();
try {
dataMap.put("killId",dto.getKillId());
dataMap.put("userId",userId);
response.setData(dataMap);
dto.setUserId(userId);
rabbitSenderService.sendKillExecuteMqMsg(dto);
}catch (Exception e){
response=new BaseResponse(StatusCode.Fail.getCode(),e. getMessage ());
}
return response;
}
(2)前端info.jsp再提交秒杀请求并接收到后端的返回信息后,便立即跳转至相应的页面,即秒杀结果查看页(准备查看相应的秒杀结果的),该页面是通过响应后端Controller器方法进行跳转的,其页面的js代码如下所示:
function executeKillMq() { $.ajax({ type: "POST", url: "${ctx}/kill/execute/mq", contentType: "application/json;charset=utf-8", data: JSON.stringify(getJsonData()), dataType: "json", success: function(res){ if (res.code==0) { //立即跳转至“秒杀结果查看页” window.location.href="${ctx}/kill/execute/mq/to/result?killId="+$("#killId").val() }else{ window.location.href="${ctx}/kill/execute/fail" } }, error: function (message) { alert("提交数据失败!"); return; } }); }
其中,Controller对应的跳转页面的方法代码如下所示:
//商品秒杀核心业务逻辑-mq限流-立马跳转至抢购结果页 @RequestMapping(value = prefix+"/execute/mq/to/result",method = RequestMethod.GET) public String executeToResult(@RequestParam Integer killId,HttpSession session,ModelMap modelMap){ Object uId=session.getAttribute("uid"); if (uId!=null){ Integer userId= (Integer)uId ; modelMap.put("killId",killId); modelMap.put("userId",userId); } return "executeMqResult"; }
其中executeMqResult.jsp主要用于查看当前用户对于当前商品的秒杀结果,页面代码比较简单,在这里就不贴出来了;下面只贴出其发起查询秒杀结果的js请求代码,如下所示:
<script type="text/javascript"> $(function () { //等待一定的时间再查询显示结果-给后端赢得足够的时间 setTimeout(showResult,5000); }); function showResult() { var killId=$("#killId").val(); var userId=$("#userId").val(); $.ajax({ type: "GET", url: "${ctx}/kill/execute/mq/result?killId="+killId+"&userId="+userId, success: function(res){ if (res.code==0) { $("#executeResult").html(res.data.executeResult); $("#waitResult").html(""); }else{ $("#executeResult").html(res.msg); } }, error: function (message) { alert("提交数据失败!"); return; } }); } </script>
其对应的Controller的请求方法如下所示:
//商品秒杀核心业务逻辑-mq限流-在抢购结果页中发起抢购结果的查询 @RequestMapping(value = prefix+"/execute/mq/result",method = RequestMethod.GET) @ResponseBody public BaseResponse executeResult(@RequestParam Integer killId,@RequestParam Integer userId){ BaseResponse response=new BaseResponse(StatusCode.Success); try { Map<String,Object> resMap=killService.checkUserKillResult(killId,userId); response.setData(resMap); }catch (Exception e){ response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage()); } return response; }
(3)其中,killService.checkUserKillResult(killId,userId);方法的功能主要是根据killId和userId在item_kill_success表查询用户的秒杀结果,其源代码如下所示:
//检查用户的秒杀结果 @Override public Map<String,Object> checkUserKillResult(Integer killId, Integer userId) throws Exception { Map<String,Object> dataMap= Maps.newHashMap(); KillSuccessUserInfo info=itemKillSuccessMapper.selectByKillIdUserId(killId,userId); if (info!=null){ dataMap.put("executeResult",String.format(env.getProperty("notice.kill.item.success.content"),info.getItemName())); dataMap.put("info",info); }else{ throw new Exception(env.getProperty("notice.kill.item.fail.content")); } return dataMap; }
而itemKillSuccessMapper.selectByKillIdUserId(killId,userId);对应的动态Sql的写法如下所示:
<!--根据秒杀成功后killId+userId的订单编码查询--> <select id="selectByKillIdUserId" resultType="com.debug.kill.model.dto.KillSuccessUserInfo"> SELECT a.*, b.user_name, b.phone, b.email, c.name AS itemName FROM item_kill_success AS a LEFT JOIN user b ON b.id = a.user_id LEFT JOIN item c ON c.id = a.item_id WHERE a.kill_id=#{killId} AND a.user_id=#{userId} AND b.is_active = 1 </select>
至此,关于RabbitMQ的接口限流篇章我们也就介绍完毕了,下面给大家展示一下整体的效果:
(1)首先当然是抢购页啦!为了区别之前的“抢购”,我们加上了一个新按钮,“抢购-MQ异步”:
(2)点击“抢购-MQ异步”按钮,前端将立即跳转至“抢购结果等待页”,如下图所示:
(3)等待一定的时间之后发起查询“秒杀结果”的请求,最终即可在页面显示秒杀的结果,如下图所示:
(4)当然,Debug还提供了一个用于JMeter压测的请求方法,代码在这里就不贴出来,可以点击文末提供的链接前往查看!不过,值得一贴的是Debug亲自压测过后的效果图,如下图所示:
至此,关于秒杀系统的优化(还有之前介绍过的分布式唯一ID、业务服务模块异步解耦、用户认证、邮件通知等也是其中的优化项)之路我们就暂时到这里了。
值得一提的是,各位小伙伴会发现我们做的这些优化大部分是“开发层面”的,而事实上,在“运维层面”也是大有文章可做的,比如我们可以:
(1)使用中间件的集群提供服务的高可用,比如Redis集群、ZooKeeper集群、RabbitMQ集群等等;
(2)Nginx集群、实现负载均衡,并从服务器的层面实现初步限流;
(3)数据库Mysql做主备部署,实现读写分离,即一个Master,多个Slave,其中Master充当写角色、Slave充当读角色,提供数据库层面的操作效率。
当然,还有很多很多,各位小伙伴有啥好的建议或者方案都可以拿出来提一提,或者加入技术群讨论讨论都是OK的!
相关视频教程可私信咨询。
推荐阅读: