Java秒杀系统实战系列~秒杀逻辑优化之RabbitMQ接口限流二

原创
2019/08/23 16:59
阅读数 157

摘要:

本篇博文是“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异步”按钮,前端将立即跳转至“抢购结果等待页”,如下图所示:

等待一定的时间之后发起查询“秒杀结果”的请求,最终即可在页面显示秒杀的结果,如下图所示:

(4)当然,Debug还提供了一个用于JMeter压测的请求方法,代码在这里就不贴出来,可以点击文末提供的链接前往查看!不过,值得一贴的是Debug亲自压测过后的效果图,如下图所示:

至此,关于秒杀系统的优化(还有之前介绍过的分布式唯一ID、业务服务模块异步解耦、用户认证、邮件通知等也是其中的优化项)之路我们就暂时到这里了。值得一提的是,各位小伙伴会发现我们做的这些优化大部分是“开发层面”的,而事实上,在“运维层面”也是大有文章可做的,比如我们可以

(1)使用中间件的集群提供服务的高可用,比如Redis集群、ZooKeeper集群、RabbitMQ集群等等

(2)Nginx集群、实现负载均衡,并从服务器的层面实现初步限流

(3)数据库Mysql做主备部署,实现读写分离,即一个Master,多个Slave,其中Master充当写角色、Slave充当读角色,提供数据库层面的操作效率。当然,还有很多很多,各位小伙伴有啥好的建议或者方案都可以拿出来提一提,或者加入技术群讨论讨论都是OK的!

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部