PHP并发问题,多个同时请求;互斥体?

标签 php concurrency eloquent mutex

所以我刚刚意识到 PHP 可能同时运行多个请求。昨晚的日志似乎显示有两个请求进来,是并行处理的;每个都触发了从另一台服务器导入数据;每个人都试图在数据库中插入一条记录。一个请求在尝试插入另一个线程刚刚插入的记录时失败(导入的数据带有 PK;我没有使用递增的 ID):SQLSTATE[23000]:完整性约束违规:1062 Duplicate entry'键 'PRIMARY' 的 865020' ....

  1. 我是否正确诊断了这个问题?
  2. 我应该如何解决这个问题?

以下是部分代码。我已经删除了大部分内容(日志记录,从数据中创建患者以外的其他实体),但以下内容应包括相关片段。请求命中 import() 方法,该方法本质上为要导入的每条记录调用 importOne()。注意 importOne() 中的 save 方法;这是一个 Eloquent 方法(使用 Laravel 和 Eloquent),它将生成 SQL 以根据需要插入/更新记录。

public function import()
{
        $now = Carbon::now();
        // Get data from the other server in the time range from last import to current import
        $calls = $this->getCalls($this->getLastImport(), $now);
        // For each call to import, insert it into the DB (or update if it already exists)
        foreach ($calls as $call) {
            $this->importOne($call);
        }
        // Update the last import time to now so that the next import uses the correct range
        $this->setLastImport($now);
}

private function importOne($call)
{
    // Get the existing patient for the call, or create a new one
    $patient = Patient::where('id', '=', $call['PatientID'])->first();
    $isNewPatient = $patient === null;
    if ($isNewPatient) {
        $patient = new Patient(array('id' => $call['PatientID']));
    }
    // Set the fields
    $patient->given_name = $call['PatientGivenName'];
    $patient->family_name = $call['PatientFamilyName'];
    // Save; will insert/update appropriately
    $patient->save();
}

我猜该解决方案需要在整个导入 block 周围使用互斥锁?如果一个请求无法获得互斥锁,它就会继续处理其余的请求。想法?

编辑:请注意,这不是严重故障。异常被捕获并记录下来,然后像往常一样响应请求。并且导入在另一个请求上成功,然后该请求将照常响应。用户并不聪明;他们甚至不知道导入,这不是请求的主要焦点。所以真的,我可以让这个运行保持原样,除了偶尔的异常,没有什么不好的事情发生。但是,如果有一个修复程序可以防止完成额外的工作/不必要地向其他服务器发送多个请求,那可能值得追求。

EDIT2:好的,我已经尝试使用flock() 实现锁定机制。想法?下面的工作吗?我将如何对这个添加进行单元测试?

public function import()
{
    try {
        $fp = fopen('/tmp/lock.txt', 'w+');
        if (flock($fp, LOCK_EX)) {
            $now = Carbon::now();
            $calls = $this->getCalls($this->getLastImport(), $now);
            foreach ($calls as $call) {
                $this->importOne($call);
            }
            $this->setLastImport($now);
            flock($fp, LOCK_UN);
            // Log success.
        } else {
            // Could not acquire file lock. Log this.
        }
        fclose($fp);
    } catch (Exception $ex) {
        // Log failure.
    }
}

EDIT3:关于锁的以下替代实现的想法:

public function import()
{
    try {
        if ($this->lock()) {
            $now = Carbon::now();
            $calls = $this->getCalls($this->getLastImport(), $now);
            foreach ($calls as $call) {
                $this->importOne($call);
            }
            $this->setLastImport($now);
            $this->unlock();
            // Log success
        } else {
            // Could not acquire DB lock. Log this.
        }
    } catch (Exception $ex) {
        // Log failure
    }
}

/**
 * Get a DB lock, returns true if successful.
 *
 * @return boolean
 */
public function lock()
{
    return DB::SELECT("SELECT GET_LOCK('lock_name', 1) AS result")[0]->result === 1;
}

/**
 * Release a DB lock, returns true if successful.
 *
 * @return boolean
 */
public function unlock()
{
    return DB::select("SELECT RELEASE_LOCK('lock_name') AS result")[0]->result === 1;
}

最佳答案

你似乎没有竞争条件,因为 ID 来自导入文件,如果你的导入算法工作正常,那么每个线程都会有自己的工作碎片,并且应该从不与他人发生冲突。现在似乎有 2 个线程正在接收创建同一个患者的请求,并且由于糟糕的算法而彼此发生冲突。

conflictfree

确保每个生成的线程从导入文件中获取一个新行,并且仅在失败时重复。

如果您不能这样做,并且想坚持使用互斥锁,那么使用文件锁似乎不是一个很好的解决方案,因为现在您解决了应用程序内部的冲突,而它实际上发生在您的数据库中。数据库锁也应该快得多,并且总体上是一个更体面的解决方案。

请求数据库锁,像这样:

$db -> exec('锁定表 table1 WRITE, table2 WRITE');

当您写入一个锁定的表时,您可能会遇到 SQL 错误,因此请在您的 Patient->save() 周围加上一个 try catch。

更好的解决方案是使用条件原子查询。也包含条件的数据库查询。您可以使用这样的查询:

INSERT INTO targetTable(field1) 
SELECT field1
FROM myTable
WHERE NOT(field1 IN (SELECT field1 FROM targetTable))

关于PHP并发问题,多个同时请求;互斥体?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30612776/

相关文章:

c++ - 与 std::unordered_map 的数据竞争,尽管使用互斥锁定插入

database - 处理非重叠范围的建议方法(例如调度)

PHP;原产地保护组织;带有下拉菜单的 MySQL INSERT 查询

java - 并发 CPLEX 进程比单个进程慢很多

php - SQL 查询导致我的加载更多帖子功能出现问题

mysql - 如何使用 Laravel 和 MySQL 处理三个或更多类的多对多关系

php - Laravel:由于某种原因无法使用

Laravel 数据透视表不是复数

php - 连接数组值,如果包含重复项 - php

javascript - 在客户端使用 jquery 将图片上传大小限制为 20kb