方案:Redis队列
原理:pop操作是原子的,即使有很多用户同时到达,也是依次执行
1.将商品库存加入队列
/**
* 商品库存放入redis
* @param string $goods_id 商品ID
* @param int $num 商品数量
* @param false $is_clear 是否清除旧库存
* @return bool
*/
public function goods_spu_redis($goods_id, $num, $is_clear=false)
{
$cache_name = 'cache_ms_goods_id_'.$goods_id;
$redis = RedisClass::getRedisConn();
//是否先清除该商品原先库存
if($is_clear)
{
while ($is_clear)
{
$is_clear = $redis->lPop($cache_name);
}
}
//库存入队
for($i=0; $i<$num; $i++)
{
$redis->lPush($cache_name,1);
}
//库存数量
$spu = $redis->lLen($cache_name);
$redis->close();
return true;
}
2.下单判断库存数量
/**
* 判断秒杀库存
* @param array $sku_data 商品数组:[{"goods_id":"1","num":"2"},{"goods_id":"2","num":"3"}]
* @return bool
*/
protected function is_spu_ms($sku_data)
{
$is_true = true;
$redis = RedisClass::getRedisConn();
foreach ($sku_data as $v)
{
$cache_name = 'cache_ms_goods_id_'.$v['goods_id'];
$spu = $redis->lLen($cache_name);
if($spu < $v['num'])
{
//库存不足
return false;
}
}
$redis->close();
return $is_true;
}
3.减库存
/**
* 秒杀减库存
* @param array $sku_data 商品数组:[{"goods_id":"1","num":"2"},{"goods_id":"2","num":"3"}]
* @return array|bool
* @throws PDOException
*/
public function reduce_spu($sku_data)
{
$this->orderM->startTrans();
$redis = RedisClass::getRedisConn();
$pipe = $redis->multi(\Redis::PIPELINE);
try
{
foreach ($sku_data as $v)
{
//redis减库存
$cache_name = CACHE_MS_NAME.$v['goods_id'];
for ($i=0; $i<$v['num']; $i++)
{
$pipe->lPop($cache_name);
}
//数据库减库存
$this->msGoodsM->where(MsGoodsM::cl_id, $v['goods_id'])->setDec(MsGoodsM::cl_spu, $v['num']);
}
$replies = $pipe->exec();
if($replies == false)
{
$redis->close();
$this->orderM->rollback();
return ['code' => 1, 'msg' => '订单生成错误,库存不足'];
}
$this->orderM->commit();
}catch (Exception $e)
{
$this->orderM->rollback();
return ['code' => 1, 'msg' => $e->getMessage()];
}
return true;
}
4.redis工具类
<?php
/**
* Created by PhpStorm.
* User: SUN
* Date: 2020/7/15
* Time: 14:16
*/
namespace app\common\controller;
use think\Env;
class RedisClass
{
const REDIS_LOCK_KEY_TEMPLATE = 'order_lock_%s';
const REDIS_LOCK_DEFAULT_EXPIRE_TIME = 60;
const REDIS_LOCK_UNIQUE_ID_KEY = 'lock_unique_id';
static $handler = null;
public static function addLock($intOrderId, $intExpireTime = self::REDIS_LOCK_DEFAULT_EXPIRE_TIME)
{
if (empty($intOrderId) || $intExpireTime <= 0) {
return false;
}
$objRedisConn = self::getRedisConn();
$intUniqueLockId = self::generateUniqueLockId();
$strKey = sprintf(self::REDIS_LOCK_KEY_TEMPLATE, $intOrderId);
$bolRes = $objRedisConn->set($strKey, $intUniqueLockId, ['nx', 'ex'=>$intExpireTime]);
return $bolRes ? $intUniqueLockId : $bolRes;
}
public static function releaseLock($intOrderId, $intLockId)
{
if (empty($intOrderId) || empty($intLockId)) {
return false;
}
$objRedisConn = self::getRedisConn();
$strKey = sprintf(self::REDIS_LOCK_KEY_TEMPLATE, $intOrderId);
$objRedisConn->watch($strKey);
if ($intLockId == $objRedisConn->get($strKey)) {
$objRedisConn->multi()->del($strKey)->exec();
return true;
}
$objRedisConn->unwatch();
return false;
}
public static function getRedisConn($select='')
{
$config['host']= Env::get('redis.host','');
$config['port']= Env::get('redis.port','');
$config['password']= Env::get('redis.password','');
$config['select']= !empty($select)?$select:Env::get('redis.select',0);
$objRedis = new \Redis();
$objRedis->connect($config['host'],$config['port']);
$objRedis->auth($config['password']);
if (0 != $config['select']) {
$objRedis->select($config['select']);
}
return $objRedis;
}
public static function generateUniqueLockId()
{
return self::getRedisConn()->incr(self::REDIS_LOCK_UNIQUE_ID_KEY);
}
}
5.并发测试
ab -r -n 6000 -c 5000 http://bid.com/api/demo/msKill
上述只是简单模拟高并发下的秒杀,真实场景要比这复杂很多
如上面的会导致一个用户抢多个
解决思路:
需要一个排队队列和抢购结果队列及库存队列。高并发情况,先将用户进入排队队列,用一个线程循环处理从排队队列取出一个用户,判断用户是否已在抢购结果队列,如果在,则已抢购,否则未抢购,库存减1,写数据库,将用户入结果队列