本篇博文是「Java秒殺系統實戰系列文章」的第十二篇,本篇博文咱們將藉助壓力測試工具Jmeter重現秒殺場景(高併發場景)下出現的各類典型的問題,其中最爲經典的當屬「商品庫存超賣」的問題,在本文咱們重現這種問題,並對問題進行分析!前端
一個正規的、聲稱能承受高併發請求的系統的背後應該經歷了一些鮮爲人知的經歷,這個秒殺系統也是如此,通常而言,這些經歷都是比較殘酷的,在本文中咱們將重現出這樣的經歷!即採用壓力測試工具Jmeter壓測這個秒殺系統的「秒殺接口」!git
在進入秒殺壓測環節前,咱們將以前的「接收前端用戶的秒殺請求對應的控制器方法」複製一份,用於給JMeter壓測使用,即在KillController中複製出一個新的「執行秒殺請求」的方法,其代碼以下所示:數據庫
//商品秒殺核心業務邏輯-用於壓力測試
@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(),"不加分佈式鎖-哈哈~商品已搶購完畢或者不在搶購時間段哦!");
}
}catch (Exception e){
response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}複製代碼
以後,咱們即可以開心的進入玩耍環節。json
(1)雙擊JMeter的啓動腳本jmeter.sh,進入JMeter的主界面,新建一個測試計劃,而後在該測試計劃下新建一個線程組(設定1秒併發1000個線程,後續還能夠調整線程數),緊接着是新建HTTP請求項以及CSV數據文件的讀取配置等等,以下圖所示:後端
其中,userId參數用於模擬參與秒殺~搶購的用戶,其取值未來源於上圖中的「CSV數據文件設置」選項的文件,在這裏Debug設定了10個用戶,以下圖所示:
bash
值得一提的,「HTTP消息頭管理器」選項是必需的,用於指定提交的數據的數據格式,即Content-Type的取值爲application/json(由於咱們的後端接口設置的就是 consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)。微信
在開始以前,咱們設定了killId=3的商品做爲秒殺~搶購的對象,並在數據表中設定其「可搶購數量/庫存」的值total爲6,以下圖所示:併發
(2)萬事俱備只欠東風,下面咱們點擊JMeter主界面的啓動,便可發起「1秒內併發1000個線程」的請求,而這1000個線程對應的用戶的Id,即userId將隨機從上述的CSV文件中讀取。在出現結果以前,咱們先從理論的角度上進行分析:10個用戶搶購庫存只有6個的書籍,那麼理論上結果應該是「庫存變爲0,被搶購完畢,而後在item_kill_success表中會有6條,並且也應該僅有6條秒殺成功的訂單記錄」! 然而,理論歸理論,現實仍是很殘酷的!app
(3)點擊JMeter的啓動按鈕,此時能夠觀察控制檯的輸出信息以及數據庫表item_kill和item_kill_success,會發現一連串「慘不忍睹」的數據記錄,以下圖所示:負載均衡
對於初次接觸「高併發秒殺業務場景」的童鞋可能會感受到驚訝,「明明通過Postman測試過了呀,爲啥還會出現這種狀況!」,有點百思不得其解!
然鵝呢,Debug想說的是「事出必有因」,而出現這種狀況,單單抱怨是屁用都木有的,還得去源頭進行分析,即從代碼的層次進行分析!
(4)咱們再次來回顧一下所寫的「秒殺接口」的核心邏輯,以下圖所示:
(1)當用戶在前端界面瘋狂的點擊「搶購」按鈕時,咱們上面接口將會接收到「洶涌潮水般」的用戶秒殺請求,首次秒殺,不少用戶都是第一次秒殺該商品,故而A流程大部分用戶都將經過考覈;
(2)同時,因爲B流程的邏輯是判斷是否可搶,而很明顯,你們都是第一次來搶的,這個商品也沒那麼快被搶完,故而B流程你們也將經過考覈;
(3)到了C流程,就須要扣減庫存了,因爲庫存的扣減在這裏只是單純的「減一」的操做,故而在C這個流程,不少人將能夠成功減一;
(4)最後你們勢如破竹,趕忙到了D流程,D流程是用於「生成秒殺成功的訂單」,記錄用戶秒殺過的商品的痕跡,同時也是爲了服務於A流程;這個時候的D已經不作什麼判斷了(你們能夠看到核心的判斷其實在於A流程,這也就是問題出現的致命根源),你們就直接插入一條成功的記錄了。
所以,最終就出現了「庫存超賣」、「同一個用戶能夠搶到屢次」等各類莫名其妙的問題;
經過上面的分析,其實Debug已經指出來了,問題產生的根源在於高併發的狀況下D流程的處理並無爲A流程的處理贏得足夠的時間,即「生成一條秒殺成功後的訂單記錄」 並無及時的爲 「判斷用戶是否已經秒殺過了~是否已經有對應的訂單記錄了」 的流程很好的服務!
那麼在下面的篇章中,咱們將從各個角度進行優化,包括數據庫級別Sql的優化、代碼邏輯的優化、分佈式鎖的引入等等(固然這些是從開發的層面來說的,其實還有運維的層面也能夠優化,好比Nginx的負載均衡、中間件的集羣部署提升高可用等等)!
一、目前,這一秒殺系統的總體構建與代碼實戰已經所有完成了,完整的源代碼數據庫地址能夠來這裏下載:gitee.com/steadyjack/… 記得Fork跟Star啊!!
二、最後,不要忘記了關注一下Debug的技術微信公衆號: