背景:一个紧俏的商品,短时间内抢购,需要支付,技术方案,
问题: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 毫秒级定时器,最大支持延迟一天。
点击了解更多去学习:非常使用的代码优化,怎么才能写好代码
本文暂时没有评论,来添加一个吧(●'◡'●)