【并发抢购】PHP+Redis抢购队列防止超卖

题记:2017过去了,公司各门店的会员一年消费所产生的积分也不少,一直在问这积分什么时候可以兑换呀?
领导前几天说要把积分兑换优惠券开放了!啊哈?积分兑换。。。?抢购优惠券???
有抢购,并发超卖的现象势必就会跟着出现,咋办捏。。锁表不现实,并发量大的情况下,锁表后MySQL的事务会对效率有很大的影响,看来只能上队列了。。。

微信图片_20180108130953.png


1.1)下单
/**
 * 会员兑换优惠券
 */
public function vipAddCoupon(){
	$openid = i('post.openid','','trim');
	$vid = i('post.vid','','trim');
	$couid = i('post.couid','','trim');
	$mid = i('post.merc','','trim');
	
	if($vid == '' || $couid == '' || $openid == '' || $mid == ''){
		$this -> error = '400004';
		return FALSE;
	}
	
	$vipData = M('vips') -> where(['vid'=>$vid]) -> where(['mid'=>$mid]) -> find();		//会员数据
	$couData = M('coupon') -> where(['couid'=>$couid]) -> find();						//优惠券数据
	
	if($couData['is_recovery'] == 1){				//优惠券停用
		$this -> error = '500031';
		return FALSE;
	}
	
	if($couData['surplus_num'] < 1){				//优惠券可换数量不足
		$this -> error = '500033';
		return FALSE;
	}
	
	if($vipData['integra'] < $couData['integra']){	//会员积分不足
		$this -> error = '500029';
		return FALSE;
	}
	
	$queueObj = A("Vipuser/Queue");					//实例化
	
	$res = $queueObj -> enqueue($couData,$vid);		//入队
	if($res['code'] != '000000'){
		$this -> error = $res['code'];
		return FALSE;
	}
	return TRUE;
}

1.png

如果进入队列返回失败(code不等于000000),直接返回抢购失败
1.2)入队处理
/**
 * 判断队满
 * @param $couData : 优惠券数据
 */
private function qIsFull($couData){
	$queueName = 'vip_coupon_queue|'.$couData['couid'];				//队列名称
    $queueSize = self::$redis -> lSize($queueName);					//获取队列长度
	if($queueSize != 0 && $queueSize >= $couData['surplus_num']){	//队列长度大于或等于可抢商品数,返出错误
		return FALSE;
	}else{
		return TRUE;
	}
}

/**
 * 队尾入队
 * @param $couData : 优惠券数据
 * @param $vid : 会员id
 */
public function enqueue($couData=null,$vid=null){
	if(is_null($couData) || is_null($vid)){
		$out_arr['code'] = '400004';
	}else{
		$res = $this -> qIsFull($couData);							//判断队列是否已满
		if(!$res){
			$out_arr['code'] = '500033';
		}else{
			$queueName = 'vip_coupon_queue|'.$couData['couid'];		//队列名称
			$re = self::$redis -> rPush($queueName,$vid);			//将会员id从队尾压入队列
			$out_arr['code'] = '000000';
		}
	}
    return $out_arr;
}

2.png


2.1)出队接口
/**
 * 队头出队
 * @param $queueName : 队列名称
 */
public function deQueue($queueName = null){
	
	if(is_null($queueName)) return FALSE;
	
	$data = $this -> getFrontData($queueName);								//获取队列长度
	if($data['code'] != '000000'){
		$out_arr['code'] = $data['code'];
	}else{
		$vid = $data['data'];
		$vipData = M('vips') -> where(['vid'=>$vid]) -> find();				//会员数据
		
		$couid = explode('|', $queueName);
		$couid = $couid[1];
		$couData = M('coupon') -> where(['couid'=>$couid]) -> find();		//优惠券数据
		
		if($couData['surplus_num'] < 1){									//优惠券可抢数不足
			$out_arr['code'] = '500033';
		}else{
			
			$addData['couid'] = $couid;
			$addData['vid'] = $vid;
			
			$obj = M('coupon_vip');											//实例化
			$obj -> startTrans();											//开启事务
			
			$addRe = M('coupon_vip') -> add($addData);						//写入会员优惠券表
			
			$save['integra'] = $vipData['integra'] - $couData['integra'];
			$re = M('vips') -> where(['vid'=>$vid]) -> save($save);			//扣减会员积分
			
			$integraData['integra'] = $couData['integra'];
			$integraData['vid'] = $vid;
			$res = M('vip_integral') -> add($integraData);					//写入会员积分变更表
			
			$couSave['surplus_num'] = $couData['surplus_num'] - 1;
			$r = M('coupon') -> where(['couid'=>$couid]) -> save($couSave);	//扣减优惠券表可用数量
			if(!$addRe || !$re || !$res || !$r){
				$obj -> rollback();											//事务回滚
				$out_arr['code'] = '999997';
				$this -> enqueue($couData,$vid);							//重新从队尾入队
			}else{
				$obj -> commit();											//提交事务
				$out_arr['code'] = '000000';
			}
		}
	}
	return json_encode($out_arr,JSON_UNESCAPED_UNICODE);
}

4.png


2.2)扫描所有优惠券、进行出队处理
/**
 * 定时任务(优惠券队列出队)
 */
public function queueSetinterval(){
	
	while (TRUE) {
		$data = M('coupon') -> where(['is_recovery'=>0]) -> where("endtime > ".time()) -> select();	//已启用、未到期的优惠券数据
		if(!empty($data)){
			foreach ($data as $k => $v) {						//因为每个优惠券的可抢数不一样,所以入队时定义的是每个优惠券分一个队列,所以这里需要循环处理
				$queueName = 'vip_coupon_queue|'.$v['couid'];	//队列名称
				$res = $this -> deQueue($queueName);			//队头出队
			}
		}else{
			sleep(5);
		}
	}
	
	die;
}

3.png


2.3)创建Linux定时任务调用优惠券出队(1秒执行一次)

5.png


3.1)创建一张优惠券
兑换规则:一共放出10000张优惠券,1积分兑换1张,每人限购80张,当前剩余20张

6.png


3.2)创建一个会员,拥有18积分

7.png


3.3)创建100个线程调用接口同时抢购,最终只有18个线程抢购成功

8.png9.png


以上就是关于抢购商品的思路及源码了

ps:感觉比小程序的实时通讯简单了N倍啊


***************当你发现自己的才华撑不起野心时,就请安静下来学习吧***************


客官,点击下方打赏一个呗~

点赞

发表评论