- 搶購、秒殺是平常很常見的場景
併發下如何解決庫存的減少超賣問題
正常是查詢出對應商品的庫存,看是否大於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
使用非阻塞的文件排他鎖