背景:一个紧俏的商品,短时间内抢购,需要支付,技术方案,
问题:1、防止超卖
2、防止重复购买
3、订单超时处理
总结:订单限额,一人一单,订单超时处理
本方案在不使用专业的队列的情况下,简单实现上述三个问题
前端:
1、ajax 发送请求,加锁,API异步完成,释放锁
var lock_status = true;
$('.btutton').click(function () {
txt=$("input").val();
if(lock_status){
//上锁
lock_status = false;
$.post("http://www.zyhuadu.com",{suggest:txt},function(result){
//回调完成 释放锁
lock_status = true;
});
}
})
后端:
技术栈:redis + swoole
第一阶段:
1、限制人数阀值
2、限制重复报名
技术方案:
1.1 、采用redis的计数器实现,具有原子性,保证不超过阀值;
1.2、采用redis的 set(集合),特性:不允许重复的成员
第二阶段:
支付问题(占位) (订单超时处理)
需求:用户报名之后,过指定时间,回访检查是否支付,若未支付则位子让出
方案一:redis 模拟实现 https://github.com/chenlinzhong/php-delayqueue
方案二:rabbitmq https://help.aliyun.com/document_detail/43349.html?spm=a2c4g.11186623.2.23.71734fed7EdaUY
方案三:Mysql+crontab 表设计时间字段,定时任务执行,查对应数据,判断状态
方案四:异步任务+swoole定时器
本文在第二阶段,选用方案四,采用Esayswoole框架,运用mysql,redis连接池,以及swoole 毫秒级定时器,异步任务
/**
* Explain: 订单限额,一人一单,订单超时处理
* User: 奔跑吧笨笨
* Date: 2019/3/11
* Time: 3:54 PM
*/
public function shopGoods()
{
$data['token'] = $this->request()->getQueryParam('token');
$data['goods_id'] = $this->request()->getQueryParam('goods_id');
$validate = new Validate();
$validate->addColumn('token')->required('非法访问,请检查token');
$validate->addColumn('goods_id')->required('请选择商品');
if(!$validate->validate($data)){
$this->writeJson(4000,$validate->getError()->__toString(),'error');
}
//2、Token 获取用户信息
$token_obj = new Token($this->getRedis());
$user_info = $token_obj->getToken($data['token']);
if(!$user_info){
$this->writeJson(4000,$user_info,'token失效');
}
//获取DB对象
$goods_db = new GoodsModel($this->getDbConnection());
$goods_order_db = new GoodsOrderModel($this->getDbConnection());
//商品信息
$goods_info = $goods_db->getOne($data['goods_id']);
//商品库存
$goods_num = $goods_order_db->goodsInventory($data['goods_id']);
if($goods_info){
//商品是否有库存
if($goods_info['number'] > $goods_num){
//用户商品是否重复下单
$key_set = self::ORDER_REDIS_ZSET.$goods_info['id'];
//生成用户值,同一商品
$key_set_value = md5($goods_info['id'].$user_info['id']);
if($this->getRedis()->sAdd($key_set,$key_set_value)){
$key_num = self::ORDER_REDIS_NUM.$goods_info['id'];
$number = $this->getRedis()->incr($key_num);
if($number <= $goods_info['number']){
$orderData['goods_name'] = $goods_info['goods_name'];
$orderData['goods_id'] = $goods_info['id'];
$orderData['number'] = 1;
$orderData['uid'] = $user_info['user']['id'];
$orderData['ctime'] = date('Y-m-d H:i:s');
//下单 入库
$result = $goods_order_db->insert($orderData);
if($result){
//设置定时器,1分钟后执行
\EasySwoole\Component\Timer::getInstance()->after(1*60*1000, function () use($result,$key_num,$key_set,$key_set_value){
$task_data['order_id'] = $result;
$task_data['cache_key'] = $key_num;
$task_data['set_cache_key'] = $key_set;
$task_data['set_cache_value'] = $key_set_value;
//投递异步任务
$taskClass = new TaskOrder(json_encode($task_data));
\EasySwoole\EasySwoole\Swoole\Task\TaskManager::async($taskClass);
});
$this->writeJson(200,$result,'success 下单成功');
}else{
$this->writeJson(4000,$orderData,'error 下单失败');
}
}else{
$this->getRedis()->decr($key_num);
$this->getRedis()->expire($key_num,self::CACHE_FAILURE_TIME);
$this->writeJson(4000,'','reids 计数器判断,该商品已售馨');
}
}else{
$this->writeJson(4000,'','抱歉,您已下单');
}
}else{
$this->writeJson(4000,'','该商品已售馨');
}
}else{
$this->writeJson(4000,'','该商品不存在或已下架');
}
}
⚠️:采用了mysql 和redis 双重判断,因为最近项目mysql 主从同步出现异常,导致读从库没有限制住,造成超卖,故优化为此方案。
<?php
/**
* Created by PhpStorm.
* User: 奔跑吧笨笨
* Date: 2019-03-10
* Time: 15:58
*/
namespace App\Task;
use App\Model\User\UserModelWithDb;
use EasySwoole\EasySwoole\Swoole\Task\AbstractAsyncTask;
use Swoole\Coroutine\Redis;
use App\Utility\Pool\RedisPool;
use EasySwoole\EasySwoole\Config;
use App\Utility\Pool\MysqlObject;
use App\Utility\Pool\MysqlPool;
use EasySwoole\Component\Pool\PoolManager;
use App\Model\Goods\GoodsModel;
use App\Model\Goods\GoodsOrderModel;
class TaskOrder extends AbstractAsyncTask
{
function run($taskData, $taskId, $fromWorkerId,$flags = null)
{
$taskData = json_decode($taskData,true);
//查询支付状态,并修改订单
$timeout = Config::getInstance()->getConf('web.MYSQL.POOL_TIME_OUT');
$mysqlObject = PoolManager::getInstance()->getPool(MysqlPool::class)->getObj($timeout);
$goods_order_db = new GoodsOrderModel($mysqlObject);
$order_info = $goods_order_db->getOne($taskData['order_id']);
if($order_info){
if($order_info['pay_status'] == 0){
//未支付,订单状态修改为2,且计数器-1
$result = $goods_order_db->updatePayStatus($taskData['order_id'],2);
if($result){
//计数器同步
$redis_timeout = Config::getInstance()->getConf('web.REDIS.POOL_TIME_OUT');
$redis = PoolManager::getInstance()->getPool(RedisPool::class)->getObj($redis_timeout);
$key = $taskData['cache_key'];
$redis->decr($key);
//重复下单 队列清除
$set_key = $taskData['set_cache_key'];
$set_key_value = $taskData['set_cache_value'];
$redis->srem($set_key,$set_key_value);
}
}
}
//回收mysql连接句柄
PoolManager::getInstance()->getPool(MysqlPool::class)->recycleObj($goods_order_db);
//回收redis连接句柄
PoolManager::getInstance()->getPool(RedisPool::class)->recycleObj($redis);
// TODO: Implement run() method.
}
function finish($result, $task_id)
{
echo "task模板任务完成\n";
return 1;
// TODO: Implement finish() method.
}
}
以上伪代码,测试使用。
注意:swoole 毫秒级定时器,最大支持延迟一天。
特此感谢:swoole开发组成员 ,swoole的出现拓宽了PHP的道路。
我为人人,人人为我,美美与共,天下大同。