PHP多进程提交时,数据重复插入。

业务逻辑:用户进行投票,投票之后写入记录;投票成功后更改用户状态,不得再投票。
直接通过postman测试接口是没问题的,数据都正常。但是只要通过多进程脚本运行测试的话,写入记录会增多。

目前想到的解决方案:

  1. 使用redis有序集合,使用时间戳毫秒写入获取第一个再做对比。简单来说,就是在毫秒维度进行做并发处理,但是感觉如果更高并发的话,应该也会出问题。
  2. 使用队列服务处理。

投票代码:

$uid = Token::getCurrentUid();
$codeNum = Token::getCurrentTokenVar('codeNum');

//并发处理
$redis = Redis::getRedisConn();
$key = RedisKeyNameLibrary::USER_VOTE.$codeNum;
$score = array_sum(explode(' ', microtime()));
$value = build_rand_str(32).':'.$uid;
$redis->zRemRangeByScore($key, 0, time() - 1);//清除1秒前的集合
$redis->zAdd($key, $score, $value);
$zRangeArr = $redis->zRange($key, 0, -1);
if ($zRangeArr[0] <> $value) return returnError('提交失敗,請重新提交!', 30002);

$tran = $this->db();
$tran->startTrans();

try{
    
    //根据ID获取对应模型
    $model = self::get($uid);
    $data = array_merge($model->toArray(), $data);
    
    //验证
    $validate = new CodeValidate();
    $result = $validate->check($data, [], 'vote');
    if(!$result) return returnError($validate->getError(), 30001);
    
    $errorMsg = returnError('提交失敗,請重新提交!', 30002);
    
    //更改为已投票
    $status = $model->data($data)->allowField(true)->save(['status' => self::STATUS_1]);
    
    //保存成功后追加投票记录
    if($status !== false) {
        $saveData = [];
        $models = User::all(array_map('intval', $data['user_ids']))->all();
        foreach($models as $k => $v) array_push($saveData, [
            'code_num' => $data['code_num'], 'uid' => $v->data['id'],
            'name' => $v->data['name'], 'group_id' => $v->data['group_id']
        ]);
        $model->logs()->saveAll($saveData);
        $tran->commit();
        return returnSuccess();
    }
    
    return $errorMsg;
}catch (Exception $e){
    $tran->rollback();
    return $errorMsg;
}    

以下是多进程测试投票接口的脚本:

for ($i = 0; $i < 6; $i ++) {
    $pid = pcntl_fork();
    if ($pid == - 1) {
        die("could not fork");
    } elseif ($pid) {
        echo "I'm the Parent $i\n";
    } else {
        $token = '123123';
        $url = 'http://api.com/user';
        $query = 'user_ids[]=1&user_ids[]=39&user_ids[]=19&user_ids[]=30';
        $command = 'curl -H "token:' . $token . '" -X POST -d "' . $query . '" ' . $url;
        // 子进程处理
        $res = system($command);
        
        file_put_contents($i . '_work.log', var_export($res, true));
        exit(); // 一定要注意退出子进程,否则pcntl_fork() 会被子进程再fork,带来处理上的影响。
    }
}

// 等待子进程执行结束
while (pcntl_waitpid(0, $status) != - 1) {
    $status = pcntl_wexitstatus($status);
    echo "Child $status completed\n";
}

想问问,除了以上解决方案之外,还有没有别的解决方案?(最好不需要启动别的服务之类的)

慕沐林林
浏览 635回答 4
4回答

拉莫斯之舞

第一:使用事务第二:update带条件 update table set user_status = 1 where user_status = 0; 第一次投票肯定是会成功的,重复的投票因为 update 语句无法执行而回滚。

ITMISS

// 假设用户 id $userId = xxxxx; $voteKey = "votes:xxx:users:{$userId}"; // 返回 0,代表当前用户已经投票过了 if (0 == Redis::hset($voteKey, 'id', $userId)) { exit('你已经投过票了'); } try { } catch (\Exception $e) { // 把当前用户踢出,给他继续投票 Redis::del($voteKey); }

叮当猫咪

投票场景要使用事务或者原子操作,如果先检查有没有投票再进行投票操作,在数据库层面,会因为同一用户相隔很近的两次请求不在同一个数据库连接中而产生并发问题。举例来说,作为同一个用户A,在很短时间内提交了2次请求1和2.1请求先到达,数据库进行处理,SELECT出来,A用户没有投票,这个时候,2请求到达了,但是请求1这时还没有更新已投票的标志位,所以SELECT出来,还是A没有投票,这时请求1里面的逻辑到了投票数量+1的时候了,之后请求2也到了投票数量+1的步骤,就产生了多次投票的情况。
打开App,查看更多内容
随时随地看视频慕课网APP