When PHP multiple processes commit, the data is inserted repeatedly.

Business logic: users vote, and records are written after voting; after successful voting, the user status is changed and no more votes are allowed.
it"s OK to pass the postman test interface directly, and the data is normal. However, as long as the test is run through a multi-process script, the number of records written will increase.

solution that comes to mind at present:

  1. use redis to order collections, use timestamp millisecond writes to get the first one, and then compare it. To put it simply, it is to do concurrency processing in the millisecond dimension, but it feels that if the concurrency is higher, it should also be a problem.
  2. is processed using the queue service.

Voting Code:

$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;
}    

the following is a script for multiple processes to test the voting interface:

for ($i = 0; $i < 6; $i PP) {
    $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";
}

I would like to ask, is there any other solution besides the above solution? (it"s better not to start other services, etc.)

Apr.23,2022

first: use transaction
second: update with condition update table set user_status = 1 where user_status = 0;

the first vote is sure to be successful, and the repeated vote is rolled back because the update statement cannot be executed.


//  id
$userId = xxxxx;


$voteKey = "votes:xxx:users:{$userId}";
//  0
if (0 == Redis::hset($voteKey, 'id', $userId)) {

    exit('');
}

try {

} catch (\Exception $e) {

    // 
    Redis::del($voteKey);
}

voting scenarios use transaction or atomic operations. If you check whether there is a vote before voting, concurrency problems will occur at the database level because two requests close to each other by the same user are not in the same database connection.
for example, as the same user A, two requests 1 and 2 are submitted in a very short time.
1 request arrives first, the database is processed, SELECT comes out, and A user does not vote. At this time, request 2 arrives, but request 1 has not updated the voted flag bit at this time, so SELECT comes out, or A does not vote. At this time, the logic in request 1 is the number of votes + 1. After that, request 2 also arrives at the step of + 1 in the number of votes, which results in multiple votes.


setnx go to redis to grab the lock, and only those who can grab the lock can vote.

Menu