Java秒杀系统(十八):秒杀逻辑优化之RabbitMQ接口限流二
作者:
修罗debug
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
摘要:本篇博文是“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、业务服务模块异步解耦、用户认证、邮件通知等也是其中的优化项)之路我们就暂时到这里了。值得一提的是,各位小伙伴会发现我们做的这些优化大部分是“开发层面”的,而事实上,在“运维层面”也是大有文章可做的,比如我们可以采取如下的措施:
A 使用中间件的集群提供服务的高可用,比如Redis集群、ZooKeeper集群、RabbitMQ集群等等
B Nginx集群、实现负载均衡,并从服务器的层面实现初步限流
C 数据库Mysql做主备部署,实现读写分离,即一个Master,多个Slave,其中Master充当写角色、Slave充当读角色,提供数据库层面的操作效率。当然,还有很多很多,各位小伙伴有啥好的建议或者方案都可以拿出来提一提,或者加入技术群讨论讨论都是OK的!
补充:
1、目前,这一秒杀系统的整体构建与代码实战已经全部完成了,该秒杀系统对应的视频教程的链接地址为:https://www.fightjava.com/web/index/course/detail/6,可以点击链接进行试看以及学习,实战期间有任何问题都可以留言或者与Debug联系、交流!
2、另外,Debug也开源了该秒杀系统对应的完整的源代码以及数据库,其地址可以来这里下载:https://gitee.com/steadyjack/SpringBoot-SecondKill 记得Fork跟Star啊!!!
3、最后,不要忘记了关注一下Debug的技术微信公众号