簡單實現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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM