php處理搶購類功能的高併發請求

本文以搶購、秒殺爲例。介紹如何在高併發狀況下確保數據正確。
在高併發請求下容易參數兩個問題
1.數據出錯,導致產品超賣。
2.頻繁操作數據庫,導致性能下降。

測試環境

Windows7
apache2.4.9
php5.5.12
php框架 yii2.0
工具 apache bench (apache自帶高併發請求工具)。

通常處理方法

從控制器可以看出代碼思路。先查詢商品庫存。如果庫存大於0
,則庫存減少1,同時生產訂單,錄入搶購者數據。

    // 常規代碼處理高併發
    public function actionNormal(){
        // 查詢庫存
        $stock = Goods::find()->select('stock')->where(['goods_id'=>100001])->asArray()->one();
        // 判斷該商品是否還有庫存
        if ($stock['stock']>0) {
            // 庫存減一
            Goods::updateAllCounters(['stock' => -1],['goods_id'=>100001]);

            // 生產訂單(另外功能,暫且隨機賦值)
            $order = $this->build_order();

            // 秒殺信息入庫
            $model = new Highly();
            $model->order_id = $order;
            $model->goods_name = '秒殺商品';
            $model->buy_time = date('Y-m-d H:i:s',time());
            $model->mircrotime = microtime(true);
            if($model->save()===false){
                echo '未能成功搶購!';
            }else{
                echo '恭喜你,訂單<b>'.$order.'</b>搶購成功';
            }

        }else{
            echo '已被搶購一空!';
        }
    }

將商品庫存設置爲20後,通過ab 配置200的併發請求。

ab -n 200 -c 200 http//localhost/highly/normal

執行結果發現庫存變成了負值,商品超賣了。
這裏寫圖片描述
原因比較簡單,在高併發請求下。在生產訂單,減少庫存之前,會優先查詢到庫存結果。

優化一:修改庫存數據類型

第一種優化方法,從數據庫入手。既然查詢到的結果不準確,那我就在庫存減少上做手腳。將庫存的數據類型改成無符號(不能有負值)。
代碼還是跟上面差不多,只是在庫存減1的地方做了個判斷。避免報錯。

public function actionNormal(){
        // 查詢庫存
        $stock = Goods::find()->select('stock')->where(['goods_id'=>100001])->asArray()->one();
        // 判斷該商品是否還有庫存
        if ($stock['stock']>0) {
            // 庫存減一
            if(Goods::updateAllCounters(['stock' => -1],['goods_id'=>100001])===false){
                echo "已被搶購一空!";
                return false;
            }

            // 生產訂單(另外功能,暫且隨機賦值)
            $order = $this->build_order();

            // 秒殺信息入庫
            $model = new Highly();
            $model->order_id = $order;
            $model->goods_name = '秒殺商品';
            $model->buy_time = date('Y-m-d H:i:s',time());
            $model->mircrotime = microtime(true);
            if($model->save()===false){
                echo '未能成功搶購!';
            }else{
                echo '恭喜你,訂單<b>'.$order.'</b>搶購成功';
            }

        }else{
            echo '已被搶購一空!';
        }
    }

這一次同樣200的併發,執行結果發現。數據正確,並不會出現超賣的情況。
思路其實也比較簡單。因爲庫存不能爲負值,當庫存等於0時,如果還有值傳進來,則會報錯。請求被終止。

這種優化方式,雖然避免了商品超賣的情況。但是在另一方面,請求仍然會對數據庫造成壓力。如果多個功能使用此數據庫,會造成性能下降厲害。

優化二:redis

利用 redis list類型的pop的原子性。在操作數據庫前,做一個驗證。當商品賣完後,就不允許再繼續進行數據庫操作。

// redis list 高併發測試
    public function actionRedis(){
        $redis = \Yii::$app->redis;
        // $redis->lpush('mytest',1);
        $order = $this->build_order();
        // echo $order;die;
        // echo $redis->llen('mytest');
        $reg = $redis->lpop('mytest');
        if (!$reg) {
            echo "笨蛋!已經被搶光啦!";
            return false;
        }
        $redis->close();
        $model = new Highly();
        $model->order_id = $order;
        $model->goods_name = '秒殺商品';
        $model->buy_time = date('Y-m-d H:i:s',time());
        $model->mircrotime = microtime(true);

        if($model->save()===false){
            echo '未能成功搶購!';
        }else{
            echo '恭喜你,訂單<b>'.$order.'</b>搶購成功';
        }
    }
    // 給redis添加商品
    public function actionInsertgoods(){
        $count = yii::$app->request->get('count',0);
        if (empty($count)) {
            echo '大兄弟,你還沒告訴我需要上架多少商品呢!';
            return false;
        }
        $redis = \Yii::$app->redis;
        for ($i=0; $i < $count; $i++) { 
            $redis->lpush('mytest',1);
        }
        echo '成功添加了'.$redis->llen('mytest').'件商品。';
        $redis->close();

    }

這點的代碼,我寫了兩個方法。第一個方法是秒殺的代碼,第二個方法是給秒殺的商品設置數量。爲了方便測試,我這裏處理的比較簡單。

通過測試,數據庫生產的訂單數量正常,並沒有出現問題。而又避免了請求數據庫造成性能下降的問題。同時內存數據庫redis查詢的速度要比mysql快很多。

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