1. 程式人生 > 實用技巧 >簡單實現redis實現高併發下的搶購/秒殺功能(轉)

簡單實現redis實現高併發下的搶購/秒殺功能(轉)

簡述

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

常規寫法:

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

這裡我就只談redis的解決方案

我們先來看以下php程式碼是否能正確解決超搶/賣的問題:

<?php

$redis = new Redis();
$redis->connect('127.0.0.1', 6379); 

  //系統庫存量
 $num = 10;  

 //當前搶購使用者id,模擬資料
 $user_id =  rand(0,100);

//檢查庫存,order:1 定義為健名 $len = $redis->llen('order:1'); if($len >= $num)   return '已經搶光了哦'; //把搶到的使用者存入到列表中 $result =$redis->lpush('order:1',$user_id); if($result)   return '恭喜您!搶到了哦';

如果程式碼正常執行,按照預期理解的是列表order:1中最多隻能儲存10個使用者的id,因為庫存只有10個。
然而,但是,在使用jmeter工具模擬多使用者併發請求時,最後發現order:1中總是超過10個使用者,也就是出現了“超賣”。

分析問題就出在這一段程式碼:

 $len = $redis->llen('order:1');  

 if($len >= $num)
   return '已經搶光了哦';

在搶購進行到一定程度,假如現在已經有9個人搶購成功,又來了3個使用者同時搶購,這時if條件將會被繞過(條件同時被滿足了),這三個使用者都能搶購成功。而實際上只剩下一件庫存可以搶了。
在高併發下,很多看似不大可能是問題的,都成了實際產生的問題了。要解決“超搶/超賣”的問題,核心在於保證檢查庫存時的操作是依次執行的,再形象的說就是把“多執行緒”轉成“單執行緒”。即使有很多使用者同時到達,也是一個個檢查並給與搶購資格,一旦庫存搶盡,後面的使用者就無法繼續了。

我們需要使用redis的原子操作來實現這個“單執行緒”。首先我們把庫存存在goods_store:1這個列表中,假設有10件庫存,就往列表中push10個數,這個數沒有實際意義,僅僅只是代表一件庫存。搶購開始後,每到來一個使用者,就從goods_store:1中pop一個數,表示使用者搶購成功。當列表為空時,表示已經被搶光了。因為列表的pop操作是原子的,即使有很多使用者同時到達,也是依次執行的。搶購的示例程式碼如下:
比如這裡我先把庫存(假設有10件)放入redis佇列:

$redis = new redis();
$redis->connect('127.0.0.1', 6379);

 //庫存
$num=10;

//檢查庫存,goods_store:1 定義為健名
$len=$redis->llen('goods_store:1'); 

//實際庫存-被搶購的庫存 = 剩餘可用庫存
$count = $num-$len; 

for($i=0;$i<$count;$i++)

//往goods_store列表中,未搶購之前這裡應該是預設滴push10個庫存數了
$redis->lpush('goods_store:1',1);

好吧,搶購時間到了:

$redis = new redis();
$redis->connect('127.0.0.1', 6379);
$user_id =  rand(0,100);//當前搶購使用者id
/* 模擬搶購操作,搶購前判斷redis佇列庫存量 */
$count=$redis->lpop('goods_store:1');
if(!$count)
  return '已經搶光了哦';

$result = $redis->lpush('order:1',$user_id);
if($result)
  return '恭喜您!搶到了哦';

注意:這裡可以不必進行資料庫操作,而是先存入佇列,操作資料庫的時間是跟使用者無關的,所以應該立馬返回讓使用者知道是否搶到,之後再對這個佇列進行操作。

為了檢測實際效果,我使用jmeter工具模擬100、500、1000個使用者併發進行搶購,經過大量的測試,最終搶購成功的使用者始終為10,沒有出現“超賣”。

問題

上面雖然能夠解決超賣的現象,但是卻不能夠防止超搶的情況發生,就是一個使用者可以搶 到相同的多件商品

嘗試解決

$data = $redis->lRange('order:1',0,-1);  //把搶到的使用者存入到列表中
if(!in_array($user_id,$data))
{
    $count=$redis->lpop('goods_store:1');
    if(!$count)
      return '已經搶光了哦';

    $result = $redis->lpush('order:1',$user_id);
    if($result)
      return '恭喜您!搶到了哦';
}
else
{
    return '已經搶購了哦';
}

上面這個程式碼在沒有高併發情況下測試沒有問題,但是如果高併發情況下呢。答案是不可以的。這跟上面的

 $len = $redis->llen('order:1');  

 if($len >= $num)
   return '已經搶光了哦';

是一樣道理的,所以行不通

這時有人提出了

$data = $redis->lRange('order:1',0,-1);  //把搶到的使用者存入到列表中
if(!in_array($user_id,$data))
...

這兩行程式碼不就是判斷list中某個值是否存在嗎?為何不直接呼叫list的exist函式判斷,我剛開始也照著這樣去查詢。不過並沒有找到這個內建函式。而我也自己寫了一個函式判斷是否存在list中,虛擬碼如下

if(呼叫函式判斷id是否存在list中)
 {
  $count=$redis->lpop('goods_store:1');
  if(!$count)
    return '已經搶光了哦';
  $result = $redis->lpush('order:1',$user_id);
  if($result)
    return '恭喜您!搶到了哦';
  }
 else
 {
    return '已經搶購了哦';
 }

不過答案還是不行的,因為如果同時有兩個相同id進入if判斷,還是都會進入到if中。

解決

這時我將list轉成了hash,將程式碼改成了下面

//把所有使用者都插入到這個佇列中
$wait_key = "user_wait:2";

//真正搶到的使用者資訊佇列
$user_key = "user:1";

//庫存佇列
$store_key = "goods_store:1";

$result =$redis->hset($wait_key, $user_id, $user_id);
    if($result)
    {
        $count = $redis->lpop($store_key);
        if (!$count)
            echo '已經搶光了哦'.$user_id;
        else
        {
            $result =$redis->hset($user_key, $user_id, $user_id);
            echo '恭喜您!搶到了哦'.$user_id;
        }
    }

這時又有人說這樣幹嘛不可以用list,程式碼如下:

//把所有使用者都插入到這個佇列中
$wait_key = "user_wait:2";

//真正搶到的使用者資訊佇列
$user_key = "user:1";

//庫存佇列
$store_key = "goods_store:1";
$result =$redis->rPush($wait_key, $user_id);
    if($result)
    {
        $count = $redis->lpop($store_key);
        if (!$count)
            echo '已經搶光了哦'.$user_id;
        else
        {
            $result =$redis->rPush($user_key,  $user_id);
            echo '恭喜您!搶到了哦'.$user_id;
        }
    }

對比

list:

hash:

分析:list中的值是可以重複的,而hash裡面的值是不可以重複的

所以

$result =$redis->rPush($wait_key, $user_id);

$result =$redis->hset($wait_key, $user_id, $user_id);

當高併發的的情況下,無論id是否相同,list的rpush返回結果都是1,而hash的hset只有不同的時候才返回1.這樣就可以避免由於高併發而導致一個使用者搶到多件同種商品

測試結果

先加入10個庫存

$user_id =  1;//當前搶購使用者id

$wait_key = "user_wait:2";

$user_key = "user:1";

$store_key = "goods_store:1";

    $result =$redis->hset($wait_key, $user_id, $user_id);
    if($result)
    {
        $count = $redis->lpop($store_key);
        if (!$count)
            echo '已經搶光了哦'.$user_id;
        else
        {
            $result =$redis->hset($user_key, $user_id, $user_id);
            echo '恭喜您!搶到了哦'.$user_id;
        }
    }
    else
    {
        echo '已經搶到了'.$user_id;
    }

為了測試極限高併發情況下,我直接將使用者Id設定為1

我這裡模擬1000個使用者同時進入秒殺

如果秒殺成功,應該是庫存為9,而真正的搶購佇列只有使用者1

使用jemter測試結果

結果也是符合預期的

現在來模擬真正不同id看看是否只是10個使用者能搶到

先補充倉庫

所有搶購使用者

真正搶購到

至此,已經算是實現了預期

轉自:https://blog.csdn.net/qq_33862778/article/details/80651703