文档章节

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

tree2013
 tree2013
发布于 2016/12/01 17:18
字数 1911
阅读 197
收藏 0

我们有个需求就是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}');//同一时刻一个团只允许一个人操作

 

© 著作权归作者所有

共有 人打赏支持
tree2013
粉丝 27
博文 194
码字总数 62280
作品 0
武汉
后端工程师
私信 提问
2017-04-17日志

工作内容: 分析团购功能的主要业务流程 数据库建表 添加后天页面后台页面路径内容没填写 考虑定时任务执行方式 拼团活动业务流程 拼团信息 后台———— 展示 -- 展示拼团活动的具体信息 如...

李敬超
2017/04/17
1
0
阿里云双11拼团活动:组团拼购1折起,拉新瓜分现金

2018阿里云双十一拼团大促活动已经于10月29日正式开启,从已开放的活动页面来看,活动分为两个阶段:10月29日-11月08日的拼团阶段和11月09日-11月12日的百团大战阶段。 活动核心亮点:组团拼...

宋庆离
2018/10/29
655
8
拼团+地推,永欣欢乐谷如何用小程序打破传统藩篱

今天给酷客多给大家带来一套优秀案例分享——通化永欣欢乐谷。 永欣欢乐谷使用酷客多小程序作为线上工具,通过微信生态圈传播推广,利用酷客多营销插件实现门票销售。配合地推宣传方式,是将...

灵动生活
2018/07/27
0
0
2018阿里云双11拼团大促主会场全攻略

摘要: 在双十一这个一年唯一一次的大幅度降价促销日,怎样才能花最少的钱配置最特惠的云服务?云栖社区特为各位开发者奉献出省钱大法如下! 2018阿里云双十一拼团大促活动已经于10月29日正式...

宋庆离
2018/10/29
57
0
2018阿里云双11拼团大促主会场全攻略

摘要: 在双十一这个一年唯一一次的大幅度降价促销日,怎样才能花最少的钱配置最特惠的云服务?云栖社区特为各位开发者奉献出省钱大法如下! 2018阿里云双十一拼团大促活动已经于10月29日正式...

阿里云云栖社区
2018/10/29
0
0

没有更多内容

加载失败,请刷新页面

加载更多

SpringMVC工作原理

SpringMVC的工作原理图: SpringMVC流程 1、 用户发送请求至前端控制器DispatcherServlet。 2、 DispatcherServlet收到请求调用HandlerMapping处理器映射器。 3、 处理器映射器找到具体的处理...

呵呵哒灬
32分钟前
1
0
数据库技术-Mysql主从复制与数据备份

数据库技术-Mysql 主从复制的原理: MySQL中数据复制的基础是二进制日志文件(binary log file)。一台MySQL数据库一旦启用二进制日志后,其作为master,它的数据库中所有操作都会以“事件”...

须臾之余
昨天
12
0
Git远程仓库——GitHub的使用(一)

Git远程仓库——GitHub的使用(一) 一 、 Git远程仓库 由于你的本地仓库和GitHub仓库之间的传输是通过SSH加密的,所以需要一下设置: 步骤一、 创建SSH key 在用户主目录下,看看有没有.ss...

lwenhao
昨天
4
0
SpringBoot 整合

springBoot 整合模板引擎 SpringBoot 整合Mybatis SpringBoot 整合redis SpringBoot 整合定时任务 SpringBoot 整合拦截器...

细节探索者
昨天
1
0
第二个JAVA应用

第二个JAVA应用 方法一:配置文件: # cd /usr/local/tomcat/conf/# vim server.xml</Host> <Host name="www.wangzb.cc" appBase="/data/wwwroot/www.wangzb.cc" //引用所......

wzb88
昨天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部