Java秒殺系統(tǒng)(十八):秒殺邏輯優(yōu)化之RabbitMQ接口限流二
作者:
修羅debug
版權聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權協(xié)議,轉(zhuǎn)載請附上原文出處鏈接和本聲明。
摘要:本篇博文是“Java秒殺系統(tǒng)實戰(zhàn)系列文章”的第十八篇,我們將繼續(xù)秒殺系統(tǒng)的優(yōu)化之路。在本篇文章中我們將基于RabbitMQ異步通信、FIFO(先進先出)、接口限流的特性,在執(zhí)行秒殺核心的處理邏輯之前架上一層“限流”的處理邏輯,從而讓瞬時產(chǎn)生的,猶如波濤洶涌、潮水般的請求流量變得井井有條、有序性地到達后端的秒殺接口!
內(nèi)容:接著上一篇章的講解,我們需要在后端 接收前端高并發(fā)產(chǎn)生多線程請求時,及時高效地轉(zhuǎn)移巨大的用戶請求之MQ中間件中,為后端秒殺接口贏得足夠的、規(guī)范化的處理!在這一過程,前端和后端的交互是異步的,因此,在前后端處理邏輯層面跟前面篇章的處理方式將有所不同。
(1)首先,在Controller層,需要提供響應前端秒殺請求的方法,該方法不直接處理秒殺的核心業(yè)務邏輯,而是將其轉(zhuǎn)移至MQ中間件中,并立即返回success的狀態(tài)信息給回到前端,其代碼如下所示:
@Autowired
private RabbitSenderService rabbitSenderService;
//商品秒殺核心業(yè)務邏輯-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再提交秒殺請求并接收到后端的返回信息后,便立即跳轉(zhuǎn)至相應的頁面,即秒殺結果查看頁(準備查看相應的秒殺結果的),該頁面是通過響應后端Controller器方法進行跳轉(zhuǎn)的,其頁面的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) {
//立即跳轉(zhuǎn)至“秒殺結果查看頁”
window.location.href="${ctx}/kill/execute/mq/to/result?killId="+$("#killId").val()
}else{
window.location.href="${ctx}/kill/execute/fail"
}
},
error: function (message) {
alert("提交數(shù)據(jù)失??!");
return;
}
});
}
其中,Controller對應的跳轉(zhuǎn)頁面的方法代碼如下所示:
//商品秒殺核心業(yè)務邏輯-mq限流-立馬跳轉(zhuǎn)至搶購結果頁
@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主要用于查看當前用戶對于當前商品的秒殺結果,頁面代碼比較簡單,在這里就不貼出來了;下面只貼出其發(fā)起查詢秒殺結果的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("提交數(shù)據(jù)失敗!");
return;
}
});
}
</script>
其對應的Controller的請求方法如下所示:
//商品秒殺核心業(yè)務邏輯-mq限流-在搶購結果頁中發(fā)起搶購結果的查詢
@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);方法的功能主要是根據(jù)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);對應的動態(tài)Sql的寫法如下所示:
<!--根據(jù)秒殺成功后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)首先當然是搶購頁啦!為了區(qū)別之前的“搶購”,我們加上了一個新按鈕,“搶購-MQ異步”:
(2)點擊“搶購-MQ異步”按鈕,前端將立即跳轉(zhuǎn)至“搶購結果等待頁”,如下圖所示:
(3)等待一定的時間之后發(fā)起查詢“秒殺結果”的請求,最終即可在頁面顯示秒殺的結果,如下圖所示:
(4)當然,Debug還提供了一個用于JMeter壓測的請求方法,代碼在這里就不貼出來,可以點擊文末提供的鏈接前往查看!不過,值得一貼的是Debug親自壓測過后的效果圖,如下圖所示:
至此,關于秒殺系統(tǒng)的優(yōu)化(還有之前介紹過的分布式唯一ID、業(yè)務服務模塊異步解耦、用戶認證、郵件通知等也是其中的優(yōu)化項)之路我們就暫時到這里了。值得一提的是,各位小伙伴會發(fā)現(xiàn)我們做的這些優(yōu)化大部分是“開發(fā)層面”的,而事實上,在“運維層面”也是大有文章可做的,比如我們可以采取如下的措施:
A 使用中間件的集群提供服務的高可用,比如Redis集群、ZooKeeper集群、RabbitMQ集群等等
B Nginx集群、實現(xiàn)負載均衡,并從服務器的層面實現(xiàn)初步限流
C 數(shù)據(jù)庫Mysql做主備部署,實現(xiàn)讀寫分離,即一個Master,多個Slave,其中Master充當寫角色、Slave充當讀角色,提供數(shù)據(jù)庫層面的操作效率。當然,還有很多很多,各位小伙伴有啥好的建議或者方案都可以拿出來提一提,或者加入技術群討論討論都是OK的!
補充:
1、目前,這一秒殺系統(tǒng)的整體構建與代碼實戰(zhàn)已經(jīng)全部完成了,該秒殺系統(tǒng)對應的視頻教程的鏈接地址為:https://www.fightjava.com/web/index/course/detail/6,可以點擊鏈接進行試看以及學習,實戰(zhàn)期間有任何問題都可以留言或者與Debug聯(lián)系、交流!
2、另外,Debug也開源了該秒殺系統(tǒng)對應的完整的源代碼以及數(shù)據(jù)庫,其地址可以來這里下載:https://gitee.com/steadyjack/SpringBoot-SecondKill 記得Fork跟Star?。。?!
3、最后,不要忘記了關注一下Debug的技術微信公眾號