PHP測試模擬併發下單,搶購

  • 搶購、秒殺是平常很常見的場景
    併發下如何解決庫存的減少超賣問題
正常是查詢出對應商品的庫存,看是否大於0,
然後執行生成訂單等操作,但是在判斷庫存是否大於0處,
如果在高併發下就會有問題,導致庫存量出現負數
  • 簡單模擬一下測試一下

  • 準備建表:庫存 - 商品 -訂單三張表,

  • 商品表bt_goods

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateBtGoodsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('bt_goods', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('goods_id')->comment('商品id');
            $table->index('goods_id');
            $table->integer('cat_id')->nullable();
            $table->string('goods_name',16)->comment('商品名稱');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('bt_goods');
    }
}

訂單表bt_orders

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateBtOrdersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('bt_orders', function (Blueprint $table) {
            $table->increments('id');
            $table->string('order_no')->comment('訂單號');
            $table->integer('user_id')->comment('用戶id');
            $table->integer('status')->comment('訂單狀態');
            $table->integer('goods_id')->comment('商品id');
            $table->index('goods_id');
            $table->integer('sku_id')->nullable();
            $table->integer('price')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('bt_orders');
    }
}

  • 庫存表bt_stock
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateBtStockTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('bt_stock', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('number')->comment('庫存');
            $table->integer('freez');
            $table->integer('goods_id')->comment('商品id');
            $table->index('goods_id');
            $table->integer('sku_id')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('bt_stock');
    }
}
  • 假設id商品爲2的庫存爲20個
    在這裏插入圖片描述

  • 測試

 <?php
namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use App\User;
use App\BtGood;
use App\BtStock;
use App\BtOrder;
use App\Notifications\TopicReplied;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class BtController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
         $this->middleware('auth');
    }

    /**
     * 模擬下單操作 庫存是否大於0
     */
    public function order()
    {
    	 $redis = app('redis');
     	 $cacheKey = "goods-stock:2";
     	 $redisLen = $redis->llen($cacheKey);
         $redisPop  = $redis->lpop($cacheKey);
		if( !$redisPop ){
           return "抱歉!";
        }
		$stock =  BtStock::where('goods_id',2)->first();
	     if( $stock->number > 0 ){
              $orderNo = $this->buildOrderNo();
              $userId = random_int(1,9999);
              //事務
              DB::beginTransaction();
              try {
              	  //鎖當前
                  BtStock::where('sku_id',66)->lockForUpdate()->first();
                  //生成訂單
                  $order = BtOrder::create(['order_no'=>$orderNo, 'user_id'=>$userId
                 ,'status'=>1, 'price'=>9999,'sku_id'=>66,'goods_id'=>2]);
                 //減少庫存
                 if ($order){
                    BtStock::where('sku_id',66)->decrement('number');
                    
                    Log::info("創建訂單成功-訂單號:{$orderNo}-----第{$order->id}個");
                    return  response()->json(['status' => 'success','code' => 200,
                    'message' => '庫存減少成功']);
               }else{
                   Log::info('失敗');
                   return '失敗';
               }
               DB::commit();
           } catch (Exception $e) {
               DB::rollback();
           }
        }else{
           Log::info("b抱歉沒了,");
           return  response()->json(['status' => 'success','code' => 500,'message' => '庫存不夠']);
        }
       }
    }
    
    //生成唯一訂單
    function buildOrderNo()
    {
        $result = '';
        $str = 'QWERTYUIOPASDFGHJKLZXVBNMqwertyuioplkjhgfdsamnbvcxz';
        for ($i=0;$i<32;$i++){
            $result .= $str[rand(0,48)];
        }
        return md5($result.time().rand(10,99));
    }

    //將商品庫存存入redis
    function saveR()
    {
        $redis = app('redis');
        $cacheKey = "goods-stock:2";
        $redisLen=$redis->llen($cacheKey);
        $stockNum = 20;
        $count = $stockNum - $redisLen;
        for( $i = 0 ; $i < $stockNum  ; $i++ ){
            $redis->lpush($cacheKey,1);
        }
        return "R庫存值數量:{$redis->llen($cacheKey)}";
    }
}
  • 模擬150併發 10次 1500請求
siege -c 150 -r 100 www.ceshi.com/order

在這裏插入圖片描述
在這裏插入圖片描述

  • 正常不做處理併發下出現庫存爲負

  • 解決1:使用redis隊列,因爲pop操作是原子的,即使有很多用戶同時到達,也是依次執行,
    -首先saveR方法將商品存redis

在這裏插入圖片描述

  • 再次測試 同樣請求正常
    在這裏插入圖片描述
  • 解決2:使用MySQL的事務,鎖住操作的行 ----測試正常
  • 其他解決
將庫存字段設爲unsigned,當庫存爲0時,因爲字段不能爲負數,將會返回false
使用非阻塞的文件排他鎖
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章