使用redis lpop
redis的list比decr有更大的优势 可自行百度 本次使用list
压测使用的的apache
ab -n 1000 -c 20 -T application/json http://test1.com/api/shop/skillsJobs?goods_id=4
-n 1000:总共发送 1000 个请求。
-c 20:并发数为 20。
-k:保持长连接。
-T "application/json":设置请求头为 application/json。
-p 指定 POST 数据文件。
数据库设计
CREATE TABLE `shop_goods` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`goods_cate_id` int(10) unsigned NOT NULL COMMENT '商品分类',
`name` varchar(255) NOT NULL COMMENT '商品名称',
`code` varchar(255) DEFAULT NULL COMMENT '商品编码',
`img_thumb` int(11) DEFAULT NULL COMMENT '商品图片URL',
`des` text COMMENT '商品描述',
`all_num` int(10) unsigned DEFAULT '0' COMMENT '商品总库存',
`status` tinyint(3) unsigned DEFAULT '1' COMMENT '状态:1可用,0不可用',
`is_delete` tinyint(3) unsigned DEFAULT '0' COMMENT '是否删除:1已删除,0未删除',
`sort` int(10) unsigned DEFAULT '0' COMMENT '排序字段',
`amount` decimal(10,2) DEFAULT '1.00' COMMENT '商品价格,默认给1',
`add_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`start_time` int(11) DEFAULT '0',
`end_time` int(11) DEFAULT '0',
`skill_state` tinyint(1) DEFAULT '0' COMMENT '是否开启秒杀0不开 1开启',
PRIMARY KEY (`id`),
KEY `goods_cate_id` (`goods_cate_id`),
CONSTRAINT `shop_goods_ibfk_1` FOREIGN KEY (`goods_cate_id`) REFERENCES `shop_goods_cate` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品表';
CREATE TABLE `shop_order` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`order_sn` varchar(50) NOT NULL COMMENT '订单号',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`goods_id` int(11) NOT NULL COMMENT '商品ID',
`price` decimal(10,2) NOT NULL COMMENT '价格',
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态',
`addtime` datetime NOT NULL COMMENT '添加时间',
`is_delete` int(11) DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `order_sn` (`order_sn`)
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表';
--- jobs队列使用的方案
CREATE TABLE `jobs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`queue` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`payload` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`attempts` int(10) unsigned NOT NULL,
`reserve_time` int(10) unsigned DEFAULT NULL,
`available_time` int(10) unsigned NOT NULL,
`create_time` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `queue` (`queue`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=51698 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
这一步不管是直接操作库还是使用jobs队列都需要提前写入到redis
写入redis库存数量
public function initSeckillStock($param)
{
$goodsId = $param['goods_id'];
$redisKey = 'goods_num_' . $goodsId;
//清楚redisKey
/**
* 1.写入秒杀商品不能再次编辑
* 2.如需要编辑则同步移除redisKey
*/
#Cache::store('redis')->delete($redisKey);
$info = $this->dao->search([
'status' => 1,
'skill_state' => 1,
'all_num' => ['>', 0],
'start_time' => ['<=', time()],
'end_time' => ['>', time()]
])
->field('id,name,status,amount,all_num,end_time,start_time,skill_state')
->where('id', $goodsId)
->find()->toArray();
if (empty($info)) {
throw new ValidateException('商品不存在或不符合秒杀条件');
}
$len = Cache::store('redis')->lLen($redisKey);
if (!$len || $len <= 0) {
for ($i = 1; $i <= $info['all_num']; $i++) {
Cache::store('redis')->lpush($redisKey, 1);
}
}
$expTime = $info['end_time'] - time();
#写入redisKey过期时间 以及商品信息检测该商品是否还需要秒杀
Cache::store('redis')->set('goods_info' . $goodsId, json_encode($info), $expTime);
return $info;
}
第一种方案直接秒杀操作数据库
public function skill($param)
{
$goodsId = $param['goods_id'];
$goodsInfoKey = 'goods_info' . $goodsId;
$redisKey = 'goods_num_' . $goodsId;
$goodsUserKey = 'seckill_users:' . $goodsId;
$goodsInfo = Cache::store('redis')->get($goodsInfoKey);
$goodsInfo = $goodsInfo ? json_decode($goodsInfo, true) : [];
if (!$goodsInfo) {
throw new ValidateException('秒杀商品不存在');
}
#验证秒杀开启时间+结束时间
$startTime = strtotime($goodsInfo['start_time']);
$endTime = strtotime($goodsInfo['end_time']);
if (time() < $startTime || time() > $endTime) {
throw new ValidateException('活动未开始或已结束');
}
$len = Cache::store('redis')->lLen($redisKey);
if (!$len || $len <= 0) {
throw new ValidateException('商品库存不足');
}
$uid = rand(1, 50); //随机生成用户id
$dataLog['uid'] = "用户" . $uid;
Db::startTrans();
try {
// 1. 防用户重复秒杀检查(使用Redis集合)
if (Cache::store('redis')->sIsMember($goodsUserKey, $uid)) {
throw new ValidateException('请勿重复参与');
}
// 自动减少一个库存
$result = $this->dao->search([])->where('id', $goodsId)->dec('all_num')->update();;
if ($result) {
$lenLpop = Cache::store('redis')->lpop($redisKey);
if (!$lenLpop) {
throw new ValidateException('商品已售罄');
}
Cache::store('redis')->sAdd($goodsUserKey, $uid);
$lens = Cache::store('redis')->lLen($redisKey);
$insert_data = [
'order_sn' => $this->buildOrderNo(), //生成订单,
'user_id' => $uid,
'goods_id' => $goodsId,
'price' => $goodsInfo['amount'],
'status' => 1,
'addtime' => date('Y-m-d H:i:s')
];
$shopOrderRepository = app()->make(ShopOrderRepository::class);
// 订单入库
$res = $shopOrderRepository->create($insert_data);
if ($res) {
$msg = "第" . $lens . "件秒杀成功";
$dataLog['信息'] = $msg;
echo $msg;
$this->writeLog($dataLog, 1);
} else {
$msg = "第" . $lens . "件秒杀失败";
$dataLog['信息'] = $msg;
echo $msg;
$this->writeLog($dataLog, 0);
}
Db::commit();
return [];
} else {
Db::rollback();
throw new ValidateException('购买失败');
}
} catch (\Exception $e) {
Db::rollback();
$dataLog['信息'] = $e->getMessage();
$this->writeLog($dataLog);
throw new ValidateException('购买失败' . $e->getMessage());
}
}
/**
* 订单号
* @return string
*/
public function buildOrderNo()
{
return date('ymd') . substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
/**
* 生成日志
*/
public function writeLog($msg, $status = 0)
{
$data['count'] = 1;
$data['status'] = 0;
$data['add_time'] = date('Y-m-d H:i:s');
$data['msg'] = $msg;
Log::info(json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
第二种使用queue队列
use think\facade\Queue;
public function skillJobs($param)
{
$goodsId = $param['goods_id'];
$goodsInfoKey = 'goods_info' . $goodsId;
$redisKey = 'goods_num_' . $goodsId;
$goodsUserKey = 'seckill_users:' . $goodsId;
$goodsInfo = Cache::store('redis')->get($goodsInfoKey);
$goodsInfo = $goodsInfo ? json_decode($goodsInfo, true) : [];
if (!$goodsInfo) {
throw new ValidateException('秒杀商品不存在');
}
$startTime = strtotime($goodsInfo['start_time']);
$endTime = strtotime($goodsInfo['end_time']);
if (time() < $startTime || time() > $endTime) {
throw new ValidateException('活动未开始或已结束');
}
$len = Cache::store('redis')->lLen($redisKey);
if (!$len || $len <= 0) {
throw new ValidateException('商品库存不足');
}
$uid = rand(0, 50); //随机生成用户id
$dataLog['uid'] = "用户" . $uid;
Db::startTrans();
try {
// 1. 防重复秒杀检查(使用Redis集合)
if (Cache::store('redis')->sIsMember($goodsUserKey, $uid)) {
throw new ValidateException('请勿重复参与');
}
$lenLpop = Cache::store('redis')->lpop($redisKey);
if (!$lenLpop) {
throw new ValidateException('商品已售罄');
}
Cache::store('redis')->sAdd($goodsUserKey, $uid);
$lens = Cache::store('redis')->lLen($redisKey);
$jobQueueName = "updateData";
// 4. 推送写入订单任务到队列
$isPushed = Queue::push('app\jobs\ShopOrderJob', [
'user_id' => $uid,
'goods_id' => $goodsId,
'price' => $goodsInfo['amount'],
'len' => $lens
], $jobQueueName);
if( $isPushed !== false ){
echo date('Y-m-d H:i:s') . " a new Hello Job is Pushed to the MQ"."<br>";
}else{
echo 'Oops, something went wrong.';
}
Db::commit();
return json()->data(['code' => 0, 'msg' => '购买成功']);
} catch (\Exception $e) {
Db::rollback();
$dataLog['信息'] = $e->getMessage();
$this->writeLog($dataLog);
throw new ValidateException('购买失败' . $e->getMessage());
}
}
队列job文件
<?php
namespace app\jobs;
use app\common\repositories\shop\ShopGoodsRepository;
use app\common\repositories\shop\ShopOrderRepository;
use think\facade\Log;
use think\queue\Job;
class ShopOrderJob
{
public function fire(Job $job, $data)
{
try {
$this->updateData($data);
} catch (\Exception $e) {
dump($e);
exception_log('任务处理失败', $e);
}
$job->delete();
}
public function updateData($data)
{
try {
$shopOrderRepository = app()->make(ShopOrderRepository::class);
$insert_data = [
'order_sn' => $this->buildOrderNo(), // 生成订单
'user_id' => $data['user_id'],
'goods_id' => $data['goods_id'],
'price' => $data['price'],
'status' => 1,
'addtime' => date('Y-m-d H:i:s')
];
$dataLog['uid'] = "用户" . $data['user_id'];
$res = $shopOrderRepository->create($insert_data);
$shopGoodsRepository = app()->make(ShopGoodsRepository::class);
$shopGoodsRepository->search(['id' => $data['goods_id']])->dec('all_num')->update();
if ($res) {
$msg = "第" . $data['len'] . "件秒杀成功";
$dataLog['信息'] = $msg;
echo $msg;
$this->writeLog($dataLog, 1);
} else {
$msg = "第" . $data['len'] . "件秒杀失败";
$dataLog['信息'] = $msg;
echo $msg;
$this->writeLog($dataLog, 0);
}
return true;
} catch (\Exception $e) {
$dataLog['uid'] = "用户" . $data['user_id'];
$dataLog['信息'] = $e->getMessage();
$this->writeLog($dataLog);
return false;
}
}
/**
* 订单号
* @return string
*/
public function buildOrderNo()
{
return date('ymd') . substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
/**
* 生成日志
*/
public function writeLog($msg, $status = 0)
{
$data['count'] = 1;
$data['status'] = $status;
$data['add_time'] = date('Y-m-d H:i:s');
$data['msg'] = $msg;
Log::info(json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
public function failed($data)
{
// ...任务达到最大重试次数后,失败了
}
}
监听queue
php think queue:work --queue updateData
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/9d66f57ab5c94a8090d8a1e5194b1214.png