搶購/秒殺是如今很常見的一個應(yīng)用場景诗充,那么高并發(fā)競爭下如何解決超搶(或超賣庫存不足為負(fù)數(shù)的問題)呢?
常規(guī)寫法:
查詢出對應(yīng)商品的庫存诱建,看是否大于0蝴蜓,然后執(zhí)行生成訂單等操作,但是在判斷庫存是否大于0處涂佃,如果在高并發(fā)下就會有問題励翼,導(dǎo)致庫存量出現(xiàn)負(fù)數(shù)
這里我就只談redis的解決方案吧...
我們先來看以下代碼(這里我以laravel為例吧)是否能正確解決超搶/賣的問題:
<?php
$num = 10; //系統(tǒng)庫存量
$user_id = \Session::get('user_id');//當(dāng)前搶購用戶id
$len = \Redis::llen('order:1'); //檢查庫存蜈敢,order:1 定義為健名
if($len >= $num)
return '已經(jīng)搶光了哦';
$result = \Redis::lpush('order:1',$user_id); //把搶到的用戶存入到列表中
if($result)
return '恭喜您!搶到了哦';
如果代碼正常運(yùn)行,按照預(yù)期理解的是列表order:1中最多只能存儲10個用戶的id汽抚,因為庫存只有10個抓狭。
然而,但是,在使用jmeter工具模擬多用戶并發(fā)請求時造烁,最后發(fā)現(xiàn)order:1中總是超過5個用戶镐侯,也就是出現(xiàn)了“超搶/超賣”瘦麸。
分析問題就出在這一段代碼:
$len = \Redis::llen('order:1'); //檢查庫存,order:1 定義為健名
if($len >= $num)
return '已經(jīng)搶光了哦';
在搶購進(jìn)行到一定程度,假如現(xiàn)在已經(jīng)有9個人搶購成功祟身,又來了3個用戶同時搶購,這時if條件將會被繞過(條件同時被滿足了)箩溃,這三個用戶都能搶購成功拉庶。而實際上只剩下一件庫存可以搶了。
在高并發(fā)下木缝,很多看似不大可能是問題的便锨,都成了實際產(chǎn)生的問題了。要解決“超搶/超賣”的問題我碟,核心在于保證檢查庫存時的操作是依次執(zhí)行的放案,再形象的說就是把“多線程”轉(zhuǎn)成“單線程”。即使有很多用戶同時到達(dá)矫俺,也是一個個檢查并給與搶購資格吱殉,一旦庫存搶盡,后面的用戶就無法繼續(xù)了厘托。
我們需要使用redis的原子操作來實現(xiàn)這個“單線程”友雳。首先我們把庫存存在goods_store:1這個列表中,假設(shè)有10件庫存催烘,就往列表中push10個數(shù)沥阱,這個數(shù)沒有實際意義,僅僅只是代表一件庫存伊群。搶購開始后考杉,每到來一個用戶,就從goods_store:1中pop一個數(shù)舰始,表示用戶搶購成功崇棠。當(dāng)列表為空時,表示已經(jīng)被搶光了丸卷。因為列表的pop操作是原子的枕稀,即使有很多用戶同時到達(dá),也是依次執(zhí)行的。搶購的示例代碼如下:
比如這里我先把庫存(可用庫存,這里我強(qiáng)調(diào)下哈,一般都是商品詳情頁搶購,后來者進(jìn)來看到的庫存可能不再是后臺系統(tǒng)配置的10個庫存數(shù)了)放入redis隊列:
$num=10; //庫存
$len=\Redis::llen('goods_store:1'); //檢查庫存,goods_store:1 定義為健名
$count = $num-$len; //實際庫存-被搶購的庫存 = 剩余可用庫存
for($i=0;$i<$count;$i++)
\Redis::lpush('goods_store:1',1);//往goods_store列表中,未搶購之前這里應(yīng)該是默認(rèn)滴push10個庫存數(shù)了
//echo \Redis::llen('goods_store:1');//未搶購之前這里就是10了
好吧萎坷,搶購時間到了:
/* 模擬搶購操作,搶購前判斷redis隊列庫存量 */
$count=\Redis::lpop('goods_store:1');//lpop是移除并返回列表的第一個元素凹联。
if(!$count)
return '已經(jīng)搶光了哦';
/* 下面處理搶購成功流程 */
\DB::table('goods')->decrement('num', 1);//減少num庫存字段
用戶搶購成功后,上面的我們也可以稍微優(yōu)化下哆档,比如我們可用將用戶ID存入了order:1列表中蔽挠。接下來我們可以引導(dǎo)這些用戶去完成訂單的其他步驟,到這里才涉及到與數(shù)據(jù)庫的交互瓜浸。最終只有很少的人走到這一步吧澳淑,也就解決的數(shù)據(jù)庫的壓力問題。
我們再改下上面的代碼:
$user_id = \Session::get('user_id');//當(dāng)前搶購用戶id
/* 模擬搶購操作,搶購前判斷redis隊列庫存量 */
$count=\Redis::lpop('goods_store:1');
if(!$count)
return '已經(jīng)搶光了哦';
$result = \Redis::lpush('order:1',$user_id);
if($result)
return '恭喜您!搶到了哦';
不過這里還存在一個問題就是一個用戶搶購多次插佛,我們繼續(xù)優(yōu)化代碼杠巡,將搶購成功的用戶放入set中,并判斷新?lián)屬彽挠脩羰欠褚呀?jīng)在set中雇寇。若存在則返回已經(jīng)搶購成功的提示氢拥。
$user_id = \Session::get('user_id');//當(dāng)前搶購用戶id
/* 模擬搶購操作,搶購前判斷redis隊列庫存量 */
$count=\Redis::lpop('goods_store:1');
if(!$count)
return '已經(jīng)搶光了哦!';
$exist_user = \Redis::sIsMember('order:1',$user_id);
if($exist_user)
return '已經(jīng)搶購成功了哦谢床!';
$result = \Redis::sAdd('order:1',$user_id);
if($result)
return '恭喜您!搶到了哦';
為了檢測實際效果兄一,我使用jmeter工具模擬100、200识腿、1000個用戶并發(fā)進(jìn)行搶購,經(jīng)過大量的測試造壮,最終搶購成功的用戶始終為10渡讼,沒有出現(xiàn)“超搶/超賣”。
上面只是簡單模擬高并發(fā)下的搶購思路耳璧,真實場景要比這復(fù)雜很多成箫,比如雙11活動遠(yuǎn)遠(yuǎn)比這更復(fù)雜多啦,很多注意的地方如搶購活動頁面做成靜態(tài)的旨枯,通過ajax調(diào)用接口等等蹬昌。