【发布时间】:2015-08-17 05:32:45
【问题描述】:
所以我刚刚意识到 PHP 可能同时运行多个请求。昨晚的日志似乎显示有两个请求进来,是并行处理的;每个都触发了从另一台服务器导入数据;每个人都试图在数据库中插入一条记录。一个请求在尝试插入另一个线程刚刚插入的记录时失败(导入的数据带有 PK;我没有使用递增的 ID):SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '865020' for key 'PRIMARY' ...。
- 我是否正确诊断了这个问题?
- 我应该如何解决这个问题?
以下是部分代码。我已经删除了大部分内容(日志记录,从数据中创建患者以外的其他实体),但以下内容应包括相关的 sn-ps。请求命中 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();
}
我猜该解决方案需要在整个导入块周围使用互斥锁?如果一个请求无法获得互斥锁,它就会继续处理其余的请求。想法?
编辑:请注意,这不是严重故障。异常被捕获并记录下来,然后像往常一样响应请求。并且导入在另一个请求上成功,然后该请求将照常响应。用户并不聪明;他们甚至不知道导入,这不是请求的主要焦点。所以真的,我可以让它保持原样运行,除了偶尔的异常,没有什么不好的事情发生。但是,如果有一个修复程序可以防止完成额外的工作/不必要地向其他服务器发送多个请求,那可能值得追求。
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;
}
【问题讨论】:
-
我什至没有阅读您的问题内容,并给您投了赞成票。感谢上帝,有人问了一个真正的问题,而不仅仅是修复这个错字,如何四舍五入,如何查询数据库!
-
是的,并发是个问题。有很多方法可以解决这个问题,视情况而定。锁定、乐观锁定、互斥令牌、咨询锁……这一切都取决于给定情况的最佳解决方案。虽然我也对一个严肃的问题感到高兴,但我不确定这是否可以在一个答案中合理回答......
-
您是否尝试过使用 memcache 构建自己的互斥体/信号量?如果只有一台服务器正在写入数据库,它将对您有所帮助。
-
用flock() 编写了一个类似互斥锁的机制……这看起来合理吗?请参阅 OP 进行编辑。知道我将如何对此进行单元测试吗?如何同时点击两次 import() 方法...?
-
提醒任何想使用文件锁的人;当您不控制服务器上文件系统的权限时,它会让人头疼。 /s
标签: php concurrency eloquent mutex