- 抢购、秒杀是平常很常见的场景
并发下如何解决库存的减少超卖问题
正常是查询出对应商品的库存,看是否大于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
使用非阻塞的文件排他锁