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