一個簡單的用redis做秒殺支撐的demo (PHP版)

用redis做秒殺的庫存扣除, 限制每個賬號只能搶購一次, 這個簡單的demo使用了string, hash, list三種基本類型.

  • 用string類型的int值來存儲剩餘庫存, 並在搶購成功後減1
  • 用hash來存儲"已搶購到"的會員的id(可以確保用戶id作爲field的唯一性). 注意: 這個hash的field對應的uid不一定搶購成功
  • 用list來保存真正搶購成功的會員id的列表, 作爲後續處理訂單的隊列

第一次寫的時候, 嘗試過使用string的bitmap來保存該會員是否搶購成功過, 但是這個在高併發時會出問題, 所以後來換成了唯一field的hash

2個文件:

  • init.php: 初始化庫存, 統計數據, 搶購成功的會員列表等
  • buy.php: 搶購

初始化

ini.php:

$m_redis = new YourRedisClass(); //redis類很多, 可以自己寫, 也可以用predis等
$m_redis->set('rush_stock', 20);//int, 可搶購的商品總數
$m_redis->set('rush_success', 0); //int, 成功的數量
$m_redis->set('rush_fail', 0); //int, 失敗的數量
$m_redis->expire('rush_queue_h', 0); //hash, 已加入搶購隊列的會員的hash記錄表(field是唯一的, 可限制每個uid只有一次), 不一定搶購成功
$m_redis->set('rush_got_uid', ''); //string, 搶購成功的會員uid記錄, 只是爲了能簡單的顯示搶到的會員.
$m_redis->del('rush_got_uid_l'); //list, 搶購成功的會員uid(方便搶購後的訂單批次處理)
echo 'success, '.date('Y-m-d H:i:s');

執行本文件, 初始化數量.

redis-cli 下執行 "mget rush_stock rush_fail rush_success rush_got_uid" 確認初始化數據

秒殺

判斷的邏輯:

    1. 庫存是否爲0, 庫存>0則進入搶購隊列
    1. 搶購隊列數據(hash)寫入成功, 則準備扣減庫存
    1. 庫存扣減成功(餘數>=0)則搶購成功, 進入訂單處理隊列(list) 目前是用string int存儲庫存, 也可以用list的item的個數來計數, 但是初始化時沒有string類型來得簡單.

buy.php

//隨機生成會員id
$uid = rand(1,200);

$m_redis = new YourRedisClass(); //redis類很多, 可以自己寫, 也可以用predis等

$key = 'rush_stock';
$q = $m_redis->get($key);

//1. 先判斷庫存數量
//庫存爲0, 直接無法進入搶購隊列
if($q < 1){
    $m_redis->incr('rush_fail');//記錄失敗的數量
    die($uid.':OutOfStock');
}

//2. 判斷該會員是否購買過 => 是否進入過隊列
$queued = $m_redis->hSet('rush_queue_h', $uid, $uid);//這裏只能判斷是否進入了搶購的隊列. 如果庫存爲0則無法進入. 進入了隊列後才能搶購
if(!$queued){
    $m_redis->incr('rush_fail');//記錄失敗的數量
    die($uid.':queue failed');
}

//讓cpu飛一會
$n = rand(20000,100000);
for($i=0; $i < $n; $i++){
    $a = rand(1,20000);
    $a = rand(1,30000);
    $a = rand(1,40000);
    $a = rand(1,50000);
    $a = rand(1,60000);
    $a = rand(1,70000);
    $a = rand(1,80000);
    $a = rand(1,90000);
}


//3. 扣減數量
$q = $m_redis->decr($key, 1);//扣減數量後會返回結果值
echo $q.' left:';


////region 如果不判斷操作後返回的結果,則可能會造成超發
//$m_redis->incr('q_success');//記錄成功的數量  ==>這個是有bug的, 不可取
//die(':success');
////endregion

if($q < 0){
    $m_redis->incr('rush_fail');//記錄失敗的數量
    die($uid.':decrease fail');
}else{
    //記錄成功的數量
    $m_redis->incr('rush_success');
    //記錄該會員已購買
    $m_redis->append('rush_got_uid', $uid.','); //字符串追加
    $m_redis->rPush('rush_got_uid_l', $uid); //list
    die($uid.':success');
}

上面的代碼中的hash保存的會員uid, 只是進入搶購隊列的會員uid, 不一定搶購成功了, 那些根本沒有進入搶購隊列的, 也不會在這個hash中, 直接因爲庫存爲0而被拒絕了.

AB壓力測試: 做一個簡單的500個併發並總計嘗試2000次的請求(測試時, win10下600個併發Nginx就掛機了)

Apache路徑\bin>ab -n 2000 -c 500 http://xxx.com/buy.php

redis-cli下執行 "mget rush_stock rush_fail rush_success rush_got_uid" 確認結果, 通過 rush_stock 的值查看可能的超發的數量

執行 "hvals rush_queue_h"可查看進入搶購隊列的用戶id, 這個數量 >= 搶購成功的用戶數量

對於list隊列的數據操作, 可以使用 BLPOP 命令, 這樣可以實現FIFO的數據處理順序.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章