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
使用非阻塞的文件排他锁
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章