搶購/秒殺是現在很常見的一個應用場景,那麼高併發競爭下如何解決超搶(或超賣庫存不足爲負數的問題)呢?laravel
常規寫法:ajax
查詢出對應商品的庫存,看是否大於0,而後執行生成訂單等操做,可是在判斷庫存是否大於0處,若是在高併發下就會有問題,致使庫存量出現負數redis
這裏我就只談redis的解決方案吧...數據庫
咱們先來看如下代碼(這裏我以laravel爲例吧)是否能正確解決超搶/賣的問題:多線程
$num = 10; //系統庫存量 $user_id = \Session::get('user_id');//當前搶購用戶id $len = \Redis::llen('order:1'); //檢查庫存,order:1 定義爲健名 if($len >= $num) return '已經搶光了哦'; $result = \Redis::lpush('order:1',$user_id); //把搶到的用戶存入到列表中 if($result) return '恭喜您!搶到了哦';
若是代碼正常運行,按照預期理解的是列表order:1中最多隻能存儲10個用戶的id,由於庫存只有10個。 然而,可是,在使用jmeter工具模擬多用戶併發請求時,最後發現order:1中老是超過5個用戶,也就是出現了「超搶/超賣」。併發
分析問題就出在這一段代碼:高併發
$len = \Redis::llen('order:1'); //檢查庫存,order:1 定義爲健名 if($len >= $num) return '已經搶光了哦';
在搶購進行到必定程度,假如如今已經有9我的搶購成功,又來了3個用戶同時搶購,這時if條件將會被繞過(條件同時被知足了),這三個用戶都能搶購成功。而實際上只剩下一件庫存能夠搶了。工具
在高併發下,不少看似不大多是問題的,都成了實際產生的問題了。**要解決「超搶/超賣」的問題,核心在於保證檢查庫存時的操做是依次執行的,再形象的說就是把「多線程」轉成「單線程」。**即便有不少用戶同時到達,也是一個個檢查並給與搶購資格,一旦庫存搶盡,後面的用戶就沒法繼續了。測試
咱們須要使用redis的原子操做來實現這個「單線程」。首先咱們把庫存存在goods_store:1這個列表中,假設有10件庫存,就往列表中push10個數,這個數沒有實際意義,僅僅只是表明一件庫存。搶購開始後,每到來一個用戶,就從goods_store:1中pop一個數,表示用戶搶購成功。當列表爲空時,表示已經被搶光了。由於列表的pop操做是原子的,即便有不少用戶同時到達,也是依次執行的。搶購的示例代碼以下:優化
好比這裏我先把庫存(可用庫存,這裏我強調下哈,通常都是商品詳情頁搶購,後來者進來看到的庫存可能再也不是後臺系統配置的10個庫存數了)放入redis隊列:
在搶購進行到必定程度,假如如今已經有9我的搶購成功,又來了3個用戶同時搶購,這時if條件將會被繞過(條件同時被知足了),這三個用戶都能搶購成功。而實際上只剩下一件庫存能夠搶了。 在高併發下,不少看似不大多是問題的,都成了實際產生的問題了。要解決「超搶/超賣」的問題,核心在於保證檢查庫存時的操做是依次執行的,再形象的說就是把「多線程」轉成「單線程」。即便有不少用戶同時到達,也是一個個檢查並給與搶購資格,一旦庫存搶盡,後面的用戶就沒法繼續了。 咱們須要使用redis的原子操做來實現這個「單線程」。首先咱們把庫存存在goods_store:1這個列表中,假設有10件庫存,就往列表中push10個數,這個數沒有實際意義,僅僅只是表明一件庫存。搶購開始後,每到來一個用戶,就從goods_store:1中pop一個數,表示用戶搶購成功。當列表爲空時,表示已經被搶光了。由於列表的pop操做是原子的,即便有不少用戶同時到達,也是依次執行的。搶購的示例代碼以下: 好比這裏我先把庫存(可用庫存,這裏我強調下哈,通常都是商品詳情頁搶購,後來者進來看到的庫存可能再也不是後臺系統配置的10個庫存數了)放入redis隊列:
好吧,搶購時間到了:
/* 模擬搶購操做,搶購前判斷redis隊列庫存量 */ $count=\Redis::lpop('goods_store:1');//lpop是移除並返回列表的第一個元素。 if(!$count) return '已經搶光了哦'; /* 下面處理搶購成功流程 */ \DB::table('goods')->decrement('num', 1);//減小num庫存字段
用戶搶購成功後,上面的咱們也能夠稍微優化下,好比咱們可用將用戶ID存入了order:1列表中。接下來咱們能夠引導這些用戶去完成訂單的其餘步驟,到這裏才涉及到與數據庫的交互。最終只有不多的人走到這一步吧,也就解決的數據庫的壓力問題。
咱們再改下上面的代碼:
$user_id = \Session::get('user_id');//當前搶購用戶id /* 模擬搶購操做,搶購前判斷redis隊列庫存量 */ $count=\Redis::lpop('goods_store:1'); if(!$count) return '已經搶光了哦'; $result = \Redis::lpush('order:1',$user_id); if($result) return '恭喜您!搶到了哦';
爲了檢測實際效果,我使用jmeter工具模擬100、200、1000個用戶併發進行搶購,通過大量的測試,最終搶購成功的用戶始終爲10,沒有出現「超搶/超賣」。
上面只是簡單模擬高併發下的搶購思路,真實場景要比這複雜不少,好比雙11活動遠遠比這更復雜多啦,不少注意的地方如搶購活動頁面作成靜態的,經過ajax調用接口
再如上面的會致使一個用戶搶多個,思路:
須要一個排隊隊列(好比:queue:1,以user_id爲值的列表)和搶購結果隊列(好比:order:1,以user_id爲值的列表)及庫存隊列(好比上面的goods_store:1)。高併發狀況,先將用戶進入排隊隊列,用一個線程循環處理從排隊隊列取出一個用戶,判斷用戶是否已在搶購結果隊列,若是在則已搶購,不然未搶購,接着執行庫存減1,寫入數據庫,將此user_id用戶同時也進入結果隊列。