拼團3人團避免人數過多的一個算法

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

相關文章
相關標籤/搜索