開發(fā)商城會遇到商品賣的超出預(yù)設(shè)?騰云網(wǎng)絡(luò)教你如何更改
2019-09-20
原始方案(失?。?/span>在每次下訂單前我們判斷促銷商品的數(shù)量夠不夠,不夠不允許下訂單,更改庫存量時加上一個條件,只更改商品庫存大于0的商品的庫存,當(dāng)時我們使用ab進行壓力測試,當(dāng)并發(fā)超過500,訪問量超過2000時,還是會出現(xiàn)超賣現(xiàn)象。
public function buyOne() { $shop = Shop::find(1); if ($shop->number > 0) { DB::update("update shop set number = number - 1 where id = 1"); } }
第1種方案:使用mysql的事務(wù)加排他鎖來解決,首先我們選擇數(shù)據(jù)庫的存儲引擎為innoDB,使用的是排他鎖實現(xiàn)的,剛開始的時候我們測試了下共享鎖,發(fā)現(xiàn)還是會出現(xiàn)超賣的現(xiàn)象。有個問題是,當(dāng)我們進行高并發(fā)測試時,對數(shù)據(jù)庫的性能影響很大,導(dǎo)致數(shù)據(jù)庫的壓力很大。
//2.利用數(shù)據(jù)庫的forupdate來加鎖(在數(shù)量少的情況下并不會出現(xiàn)問題,但是當(dāng)并發(fā)達到(ab -n 1000 -c 200),
//就會出現(xiàn)請求非2XX的響應(yīng)增多,1000 失敗了 60)time per request 65.195
//在高并發(fā)的情況下,會導(dǎo)致數(shù)據(jù)庫連接數(shù)不夠,部分php獲取不到連接而報錯,或者是超過等待時間而報錯
public function indexMysql() { DB::beginTransaction(); //通過for update 加排它鎖 $shop = DB::table('shop')->where('id', '=', 1)->lockForUpdate()->first(); if ($shop->number > 0) { if (DB::update("update shop set number = number - 1 where id = 1")) { DB::commit(); } else { DB::rollBack();//回滾并重試 usleep(100000); $this->indexMysql(); } } else { DB::commit(); } }
第2種方案:使用文件鎖實現(xiàn)。當(dāng)用戶搶到一件促銷商品后先觸發(fā)文件鎖,防止其他用戶進入,該用戶搶到促銷品后再解開文件鎖,放其他用戶進行操作。這樣可以解決超賣的問題,但是會導(dǎo)致文件得I/O開銷很大。
第3種方案:使用redis的setnx來實現(xiàn)鎖機制。但是并發(fā)大的情況下,鎖的爭奪會變多,導(dǎo)致響應(yīng)越來越慢。(與第四種方案類似)
//在數(shù)量少的情況下并不會出現(xiàn)問題,但是當(dāng)并發(fā)達到(ab -n 1000 -c 200 就會出現(xiàn)請求非2XX的響應(yīng)增多,1000 失敗了 54) time per request 127.575
public function index() { //測試并發(fā)超賣現(xiàn)象 if (Redis::setnx(self::KEY, 1)) {//拿到了鎖 $this->buy(); } else { usleep(100000);//等會再去拿鎖 //Log::info("未爭奪到鎖,睡眠100ms"); $this->index(); } }
private function buy() { $shop = Shop::find(1); if ($shop->number > 0) { $shop->number --; $shop->save(); } Redis::del(self::KEY); }
第4種方案:redis的隊列來實現(xiàn)。將要促銷的商品數(shù)量以隊列的方式存入redis中,每當(dāng)用戶搶到一件促銷商品則從隊列中刪除一個數(shù)據(jù),確保商品不會超賣。這個操作起來很方便,而且效率極高
//4.使用redis隊列來,用戶過來直接入隊列,然后再將操作更新到數(shù)據(jù)庫
//最佳體驗(redis pconnect 9.481s, 無丟失, 無框架)
public function push() { //入隊列 Redis::lpush(self::QUEUE, 1); }
//腳本調(diào)用pop方法 * * * * * php xxx.php
public function pop()
{
while (($key = Redis::rpop(self::QUEUE))) {
$shop = Shop::find(1);
if ($shop->number > 0) {
DB::update("update shop set number = number - 1 where id = 1")
}
}
}