咱们有个需求就是3人成一个团,发起者开团,也就是剩余2我的能够参与拼团,但实际上会碰到这种状况,这个团若是进入的人过多都购买引发这个团实际超出3人的状况。所以每一个用户在进入的时候有个锁定名额的逻辑,一我的进去就会锁定五分钟,我以前作了个版本,可是代码比较复杂,php
1、基础实现redis
下面是V1版本代码:json
/** * 锁定某个拼团,去尝试锁定1,2个名额,都被锁定则则返回失败 * @param $open_id * @param $active_id * @param $team_id * @return int */ public function lockTeam1($active_id,$team_id,$open_id,$team_user_count=1){ $seetPosition=[]; //计算还有几个名额剩下 for($i=1;$i<=3;$i++){ if($i<=$team_user_count){ continue; } array_push($seetPosition,'seet_'.$i); } //返回第一个未锁定的名额 $keyTemplateSeat = SPELL_GROUP_LOCK_TEAM; $input = ['teamId'=>$team_id];//第一个拼团名额 $teamKey = $this->swtichRedisKey($input, $keyTemplateSeat); $unlockSeat=''; $lockTimeArr=[]; $firstLockTime=0; foreach($seetPosition as $val){ //echo $teamKey.$val."\n"; $lockInfo=$this->getRedis()->WYget($teamKey.$val); if($lockInfo){ array_push($lockTimeArr,$lockInfo);//将全部当时锁定的日期时间戳放入锁定数组 continue; } else{ $unlockSeat=$val; break; } } if(!empty($lockTimeArr)){ sort($lockTimeArr);//按从小到大排序 $firstLockTime=$lockTimeArr[0];//更新为最早锁定时间 } //读取用户锁定的team $keyTemplate = SPELL_GROUP_LOCK_TEAM_USER; $input = ['openId'=>$open_id]; $userLockKey = $this->swtichRedisKey($input, $keyTemplate); $userLockedTeam=$this->getRedis()->WYhGet($userLockKey,'team_id');//读取用户锁定的Team //若是用户锁定过这个team则直接返回 if($userLockedTeam==$team_id){ $ret=array('code'=>1,'msg'=>'用户锁定的跟以前锁定的位置是一个团'); return $ret; } if($unlockSeat==''){ //团已经满3人了或锁定人数满了 $ret=array('code'=>0,'msg'=>'这个团已经满员了'); if($firstLockTime){ $sUnLockTime=$firstLockTime+self::LOCK_TEAM_EXPIRE; $ret['data']=array( 'sTeamId'=>$team_id, 'sActiveId'=>$active_id, 'sLockTime'=>$firstLockTime, 'sUnLockTime'=>(string)$sUnLockTime, ); } return $ret;//没有空位 } //开始锁定 $keyTemplate = SPELL_GROUP_LOCK_INCR; $input = ['teamId'=>$team_id];// $lockredisKey = $this->swtichRedisKey($input, $keyTemplate); //这个团的这个位置只能锁定一次,不然就是锁定人数过多 if($this->getRedis()->WYincr($lockredisKey.$unlockSeat)==1){ //锁定过其余团先解锁其余团 if($userLockedTeam){ $userLockPostion=$this->getRedis()->WYhGet($userLockKey,'team_pos');//读取用户锁定的position $input = ['teamId'=>$userLockedTeam]; $unlockteamKey = $this->swtichRedisKey($input, $keyTemplateSeat);//须要解锁的团的key名 $this->getRedis()->WYdelete($unlockteamKey.$userLockPostion);//解锁用户锁定的团锁定的位置 } $this->getRedis()->WYset($teamKey.$unlockSeat,time(),self::LOCK_TEAM_EXPIRE);//标识这个团的这个位置被锁定了 $this->getRedis()->WYhMset($userLockKey,array('team_id'=>$team_id,'team_pos'=>$unlockSeat),self::LOCK_TEAM_EXPIRE);//设置用户锁定的团为当前团及锁定位置 $this->getRedis()->WYdelete($lockredisKey.$unlockSeat);//解除INCR $ret=array('code'=>1,'msg'=>'用户['.$open_id.']已经成功锁定'); return $ret; } //同时争抢这个位置的人过多 $ret=array('code'=>2,'msg'=>'锁定的人数已满'); return $ret; }
一共用了以下的Redis,第一个是避免高并发的string,第二个SPELL_GROUP_LOCK_TEAM是个string,这个是保存了团的某个位置的锁定时间(teamid_seat_1,teamid_seat_2),第三个就是用户的锁定信息,用于解锁团,这个版本操做的Redis比较多,可靠性还能够,就是逻辑比较复杂,通常人看不懂。数组
'SPELL_GROUP_LOCK_INCR', 'lock_team_incr_{#teamId}');//避免对团的锁定多用户同时
'SPELL_GROUP_LOCK_TEAM', 'user_lock_team_{#teamId}');//团的锁定位置
'SPELL_GROUP_LOCK_TEAM_USER', 'lock_team_user_new_{#openId}');//用户锁定的并发
2、list版本高并发
下面这个版本算是重构版,代码简洁点,用list结构保存了用户的每次锁团信息,一次性所有读取出来而后根据时间判断,将全部过时的信息移除队列,这个版本已经很优化了,减小了很多KEY,这个版本没有去考虑用户去锁定其余团的时候解锁当前团的问题,须要优化下:优化
/** * V2版本锁团,还未验证 * @param $active_id * @param $team_id * @param $open_id * @param int $team_user_count */ public function lockTeam($active_id,$team_id,$open_id,$team_user_count=1){ //开始锁定 $keyTemplate = SPELL_GROUP_LOCK_INCR_V2; $input = ['teamId'=>$team_id]; $lockTeamKey = $this->swtichRedisKey($input, $keyTemplate); //同一时刻这个团只容许一我的操做,避免人数过多引发错误 if($this->getRedis()->WYincr($lockTeamKey)==1) { $keyTemplate = SPELL_GROUP_LOCK_TEAM_LIST; $input = ['teamId'=>$team_id]; $UserLockTeamKey = $this->swtichRedisKey($input, $keyTemplate); $length = $this->getRedis()->WYlLen($UserLockTeamKey);//读取队列的长度 $time = time(); $flag = false; if ($length) { $lockData = $this->getRedis()->WYlRange($UserLockTeamKey);//由于key自己不大,lrange没有多大开销 krsort($lockData);//将取出的数据倒排,便于将过时的key移除 foreach ($lockData as $val) { $lData = json_decode($val, true); //当前用户再次锁定而且没有过时则直接返回,若是有未过时的锁定则直接返回 if (($lData['open_id'] == $open_id) && ($time <= $lData['lock_time'] + self::LOCK_TEAM_EXPIRE)) { $flag = true; } //过时的数据清理掉 if ($time > $lData['lock_time'] + self::LOCK_TEAM_EXPIRE) { $this->getRedis()->WYrPop($UserLockTeamKey); } } $length = $this->getRedis()->WYlLen($UserLockTeamKey);//获取新的队列长度 } //当前用户存在未过时的锁定,直接能够返回 if ($flag) { $ret=array('code' => 1, 'msg' => '用户[' . $open_id . ']存在未过时的锁定'); } else{ $maxListLength = 3 - $team_user_count;//队列容许的最大长度为总数减去剩余未支付人数 $data = json_encode(array('open_id' => $open_id,'lock_time' => $time)); if ($maxListLength > $length) { $this->getRedis()->WYlPush($UserLockTeamKey, $data);//未满就直接插入 $ret=array('code' => 1, 'msg' => '用户[' . $open_id . ']成功锁定'); } else { $ret=array('code' => 0, 'msg' => '锁定人数过多'); } } $this->getRedis()->WYdelete($lockTeamKey);//解除INCR return $ret; } return array('code' => 0, 'msg' => '同时操做的人太多了'); }
使用了以下Redis,若是加上用户,也是3个Redisthis
'SPELL_GROUP_LOCK_TEAM_LIST', 'user_lock_team_list_{#teamId}');//团锁定的队列
'SPELL_GROUP_LOCK_INCR_V2', 'lock_team_incr_v2_{#teamId}');//同一时刻一个团只容许一我的操做code
3、zset版本排序
下面这个版本是list版本的优化版,用zset储存了用户的参与时间,利用zset自然的排序功能,不用再次排序,而且删除用户锁定的团也是很容易的事情:
/** * V2版本锁团,还未验证 * @param $active_id * @param $team_id * @param $open_id * @param int $team_user_count */ public function lockTeam($active_id,$team_id,$open_id,$team_user_count=1){ //开始锁定 $keyTemplate = SPELL_GROUP_LOCK_INCR_V2; $input = ['teamId'=>$team_id]; $lockTeamKey = $this->swtichRedisKey($input, $keyTemplate); $firstLockTime='';//第一个锁定人的锁定时间 //同一时刻这个团只容许一我的操做,避免人数过多引发错误 if($this->getRedis()->WYincr($lockTeamKey)==1) { //读取用户锁定的团,若是存在则删除 $keyTemplate = SPELL_GROUP_LOCK_TEAM_USER_V2; $input = ['openId'=>$open_id]; $userLockTeamKey = $this->swtichRedisKey($input, $keyTemplate); $userLockTeam = $this->getRedis()->WYget($userLockTeamKey);//读取用户锁定的团 $keyTemplate = SPELL_GROUP_LOCK_TEAM_ZSETS; $input = ['teamId'=>$team_id]; $LockTeamSetsKey = $this->swtichRedisKey($input, $keyTemplate); //当用户已经锁定过而且锁定的不是当前的团的时候,将以前锁定的删除掉 if($userLockTeam && $userLockTeam!=$team_id){ $this->getRedis()->WYzRem($LockTeamSetsKey,$open_id);//将用户锁定的其余团解锁 } $length = $this->getRedis()->WYzCard($LockTeamSetsKey);//读取队列的长度 $time = time(); $flag = false; if ($length) { $lockData = $this->getRedis()->WYzRange($LockTeamSetsKey,0,-1,1);//查询score foreach ($lockData as $key=>$val) { //读取并设定第一个锁定人的锁定时间 if($firstLockTime==''){ $firstLockTime=$val; } //当前用户再次锁定而且没有过时则直接返回,若是有未过时的锁定则直接返回 if (($key == $open_id) && ($time <= $val + self::LOCK_TEAM_EXPIRE)) { $flag = true; } //过时的数据清理掉 if ($time > $val + self::LOCK_TEAM_EXPIRE) { $this->getRedis()->WYzRem($LockTeamSetsKey,$key); } } $length = $this->getRedis()->WYzCard($LockTeamSetsKey);//获取新的队列长度 } //当前用户存在未过时的锁定,直接能够返回 if ($flag) { $ret=array('code' => 1, 'msg' => '用户[' . $open_id . ']存在未过时的锁定'); } else{ $maxListLength = 3 - $team_user_count;//队列容许的最大长度为总数减去剩余未支付人数 if ($maxListLength > $length) { $this->getRedis()->WYzAdd($LockTeamSetsKey, $time,$open_id);//未满就直接插入 $this->getRedis()->WYexpire($LockTeamSetsKey,self::TEAM_EXPIRE);//设置过时时间,有人操做会自动延时,不然会过时 $this->getRedis()->WYset($userLockTeamKey,$team_id,self::LOCK_TEAM_EXPIRE);//设置用户当前锁定的团,有效期跟锁定团的有效期相同 $ret=array('code' => 1, 'msg' => '用户[' . $open_id . ']成功锁定'); } else { //print_r($this->getRedis()->WYzRange($LockTeamSetsKey,0,-1,1)); $ret=array('code' => 0, 'msg' => '锁定人数过多'); if($firstLockTime){ $sUnLockTime=$firstLockTime+self::LOCK_TEAM_EXPIRE; $ret['data']=array( 'sTeamId'=>$team_id, 'sActiveId'=>$active_id, 'sLockTime'=>(string)$firstLockTime, 'sUnLockTime'=>(string)$sUnLockTime, ); } } } $this->getRedis()->WYdelete($lockTeamKey);//解除INCR return $ret; } return array('code' => 0, 'msg' => '同时操做的人太多了'); }
使用了以下redis的:
'SPELL_GROUP_LOCK_TEAM_ZSETS', 'user_lock_team_zset_{#teamId}');//团锁定的有序集合 'SPELL_GROUP_LOCK_TEAM_USER_V2', 'lock_team_user_v2_{#openId}');//用户当前锁定的团 'SPELL_GROUP_LOCK_INCR_V2', 'lock_team_incr_v2_{#teamId}');//同一时刻一个团只容许一我的操做