1. 程式人生 > >【搶購/秒殺】redis實現高併發下的搶購/秒殺功能

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

開發十年,就只剩下這套架構體系了! >>>   

問題:

搶購/秒殺是如今很常見的一個應用場景,那麼高併發競爭下如何解決超搶(或超賣庫存不足為負數的問題)呢?

常規寫法:

查詢出對應商品的庫存,看是否大於0,然後執行生成訂單等操作,但是在判斷庫存是否大於0處,如果在高併發下就會有問題,導致庫存量出現負數

分析 & 方案

這裡我就只談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使用者同時也