Java秒殺系統(tǒng)(十二):JMeter壓力測試重現(xiàn)秒殺場景中超賣等問題

作者: 修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接和本聲明。



摘要:本篇博文是“Java秒殺系統(tǒng)實戰(zhàn)系列文章”的第十二篇,本篇博文我們將借助壓力測試工具Jmeter重現(xiàn)秒殺場景(高并發(fā)場景)下出現(xiàn)的各種典型的問題,其中最為經(jīng)典的當屬“商品庫存超賣”的問題,在本文我們重現(xiàn)這種問題,并對問題進行分析!

內(nèi)容:一個正規(guī)的、聲稱能承受高并發(fā)請求的系統(tǒng)的背后應(yīng)該經(jīng)歷了一些不為人知的經(jīng)歷,這個秒殺系統(tǒng)也是如此,一般而言,這些經(jīng)歷都是比較殘酷的,在本文中我們將重現(xiàn)出這樣的經(jīng)歷!即采用壓力測試工具Jmeter壓測這個秒殺系統(tǒng)的“秒殺接口”!

在進入秒殺壓測環(huán)節(jié)前,我們將之前的“接收前端用戶的秒殺請求對應(yīng)的控制器方法”復(fù)制一份,用于給JMeter壓測使用,即在KillController中復(fù)制出一個新的“執(zhí)行秒殺請求”的方法,其代碼如下所示:

//商品秒殺核心業(yè)務(wù)邏輯-用于壓力測試
@RequestMapping(value = prefix+"/execute/lock",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public BaseResponse executeLock(@RequestBody @Validated KillDto dto, BindingResult result){
if (result.hasErrors() || dto.getKillId()<=0){
return new BaseResponse(StatusCode.InvalidParams);
}
BaseResponse response=new BaseResponse(StatusCode.Success);
try {
//不加分布式鎖的前提
Boolean res=killService.killItem(dto.getKillId(),dto.getUserId());
if (!res){
return new BaseResponse(StatusCode.Fail.getCode(),"不加分布式鎖-哈哈~商品已搶購?fù)戤吇蛘卟辉趽屬彆r間段哦!");
}
}catch (Exception e){
response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}

之后,我們便可以開心的進入玩耍環(huán)節(jié)。

(1) 雙擊JMeter的啟動腳本jmeter.sh,進入JMeter的主界面,新建一個測試計劃,然后在該測試計劃下新建一個線程組(設(shè)定1秒并發(fā)1000個線程,后續(xù)還可以調(diào)整線程數(shù)),緊接著是新建HTTP請求項以及CSV數(shù)據(jù)文件的讀取配置等等,如下圖所示:


其中,userId參數(shù)用于模擬參與秒殺~搶購的用戶,其取值將來源于上圖中的“CSV數(shù)據(jù)文件設(shè)置”選項的文件,在這里Debug設(shè)定了10個用戶,如下圖所示:  


值得一提的,“HTTP消息頭管理器”選項是必需的,用于指定提交的數(shù)據(jù)的數(shù)據(jù)格式,即Content-Type的取值為application/json(因為我們的后端接口設(shè)置的就是 consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)。

在開始之前,我們設(shè)定了killId=3的商品作為秒殺~搶購的對象,并在數(shù)據(jù)表中設(shè)定其“可搶購數(shù)量/庫存”的值total為6,如下圖所示:


(2) 萬事俱備只欠東風,下面我們點擊JMeter主界面的啟動,即可發(fā)起“1秒內(nèi)并發(fā)1000個線程”的請求,而這1000個線程對應(yīng)的用戶的Id,即userId將隨機從上述的CSV文件中讀取。在出現(xiàn)結(jié)果之前,我們先從理論的角度上進行分析:10個用戶搶購庫存只有6個的書籍,那么理論上結(jié)果應(yīng)該是“庫存變?yōu)?,被搶購?fù)戤?,然后在item_kill_success表中會有6條,而且也應(yīng)該僅有6條秒殺成功的訂單記錄”! 然而,理論歸理論,現(xiàn)實還是很殘酷的!

(3) 點擊JMeter的啟動按鈕,此時可以觀察控制臺的輸出信息以及數(shù)據(jù)庫表item_kill和item_kill_success,會發(fā)現(xiàn)一連串“慘不忍睹”的數(shù)據(jù)記錄,如下圖所示:


對于初次接觸“高并發(fā)秒殺業(yè)務(wù)場景”的童鞋可能會感覺到驚訝,“明明經(jīng)過Postman測試過了呀,為啥還會出現(xiàn)這種情況!”,有點百思不得其解!

然鵝呢,Debug想說的是“事出必有因”,而出現(xiàn)這種情況,單單抱怨是屁用都木有的,還得去源頭進行分析,即從代碼的層次進行分析!

(4) 我們再次來回顧一下所寫的“秒殺接口”的核心邏輯,如下圖所示:


A 當用戶在前端界面瘋狂的點擊“搶購”按鈕時,我們上面接口將會接收到“洶涌潮水般”的用戶秒殺請求,首次秒殺,很多用戶都是第一次秒殺該商品,故而A流程大部分用戶都將通過考核;

B 同時,由于B流程的邏輯是判斷是否可搶,而很明顯,大家都是第一次來搶的,這個商品也沒那么快被搶完,故而B流程大家也將通過考核;

C 到了C流程,就需要扣減庫存了,由于庫存的扣減在這里只是單純的“減一”的操作,故而在C這個流程,很多人將可以成功減一;

D 最后大家勢如破竹,趕緊到了D流程,D流程是用于“生成秒殺成功的訂單”,記錄用戶秒殺過的商品的痕跡,同時也是為了服務(wù)于A流程;這個時候的D已經(jīng)不做什么判斷了(大家可以看到核心的判斷其實在于A流程,這也就是問題出現(xiàn)的致命根源),大家就直接插入一條成功的記錄了。

因此,最終就出現(xiàn)了“庫存超賣”、“同一個用戶可以搶到多次”等各種莫名其妙的問題;

通過上面的分析,其實Debug已經(jīng)指出來了,問題產(chǎn)生的根源在于高并發(fā)的情況下D流程的處理并沒有為A流程的處理贏得足夠的時間,即“生成一條秒殺成功后的訂單記錄” 并沒有及時的為 “判斷用戶是否已經(jīng)秒殺過了~是否已經(jīng)有對應(yīng)的訂單記錄了” 的流程很好的服務(wù)!

那么在下面的篇章中,我們將從各個角度進行優(yōu)化,包括數(shù)據(jù)庫級別Sql的優(yōu)化、代碼邏輯的優(yōu)化、分布式鎖的引入等等(當然這些是從開發(fā)的層面來講的,其實還有運維的層面也可以優(yōu)化,比如Nginx的負載均衡、中間件的集群部署提高高可用等等)!

補充:

1、目前,這一秒殺系統(tǒng)的整體構(gòu)建與代碼實戰(zhàn)已經(jīng)全部完成了,該秒殺系統(tǒng)對應(yīng)的視頻教程的鏈接地址為:https://www.fightjava.com/web/index/course/detail/6,可以點擊鏈接進行試看以及學習,實戰(zhàn)期間有任何問題都可以留言或者與Debug聯(lián)系、交流!

2、另外,Debug也開源了該秒殺系統(tǒng)對應(yīng)的完整的源代碼以及數(shù)據(jù)庫,其地址可以來這里下載:https://gitee.com/steadyjack/SpringBoot-SecondKill 記得Fork跟Star?。。?!

3、最后,不要忘記了關(guān)注一下Debug的技術(shù)微信公眾號: