咱們有個需求就是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}');//同一時刻一個團只容許一我的操做