用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" 確認初始化數據
秒殺
判斷的邏輯:
-
- 庫存是否爲0, 庫存>0則進入搶購隊列
-
- 搶購隊列數據(hash)寫入成功, 則準備扣減庫存
-
- 庫存扣減成功(餘數>=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的數據處理順序.