PHP并发问题,多个并发请求;互斥锁?
所以我刚刚意识到PHP可能会同时运行多个请求。昨晚的日志似乎显示,有两个请求是并行处理的;每个都触发了从另一台服务器导入数据;每个都尝试将记录插入到数据库中。一个请求在尝试插入另一个线程刚刚插入的记录时失败(导入的数据随 PK 一起提供;我没有使用递增的 ID):.SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '865020' for key 'PRIMARY' ...
- 我是否正确诊断了此问题?
- 我应该如何解决这个问题?
以下是一些代码。我已经剥离了其中的大部分(日志记录,从数据中创建患者以外的其他实体),但以下内容应包括相关片段。请求命中 import() 方法,该方法实质上为每个要导入的记录调用 importOne()。注意 importOne() 中的保存方法;这是一个 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();
}
我猜解决方案需要围绕整个导入块的互斥体?如果请求无法获得互斥体,它只需继续处理请求的其余部分即可。思潮?
编辑:只是要注意,这不是一个严重的故障。捕获并记录异常,然后像往常一样响应请求。导入在另一个请求上成功,然后像往常一样响应该请求。用户不是更明智的;他们甚至不知道导入,这不是请求的主要焦点。所以真的,我可以让它保持原样,除了偶尔的例外,没有什么不好的事情发生。但是,如果有一个修复程序可以防止完成其他工作/不必要地将多个请求发送到该其他服务器,那么这可能值得追求。
编辑2:好的,我已经尝试使用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.
}
}
编辑3:关于锁的以下替代实现的想法:
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;
}