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的技術微信公眾號