拼团3人团避免人数过多的一个算法

我们有个需求就是3人成一个团,发起者开团,也就是剩余2个人可以参与拼团,但实际上会碰到这种情况,这个团如果进入的人过多都购买引起这个团实际超出3人的情况。因此每个用户在进入的时候有个锁定名额的逻辑,一个人进去就会锁定五分钟,我之前做了个版本,但是代码比较复杂,

一、基础实现

下面是V1版本代码:

/**
     * 锁定某个拼团,去尝试锁定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}');//用户锁定的

二、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个Redis

'SPELL_GROUP_LOCK_TEAM_LIST', 'user_lock_team_list_{#teamId}');//团锁定的队列
'SPELL_GROUP_LOCK_INCR_V2', 'lock_team_incr_v2_{#teamId}');//同一时刻一个团只允许一个人操作

三、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}');//同一时刻一个团只允许一个人操作

 

转载于:https://my.oschina.net/u/1186749/blog/798267

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值