【搶購/秒殺】redis實現高併發下的搶購/秒殺功能

問題:

搶購/秒殺是現在很常見的一個應用場景,那麼高併發競爭下如何解決超搶(或超賣庫存不足爲負數的問題)呢?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用戶同時也進入結果隊列。

相關文章
相關標籤/搜索