Итак, я только что понял, что PHP потенциально запускает несколько запросов одновременно. Журналы прошлой ночи, похоже, показывают, что вступили два запроса, были обработаны параллельно; каждый инициировал импорт данных с другого сервера; каждая попытка вставить запись в базу данных. Один запрос не удался, когда он попытался вставить запись, которую только что вставил другой поток (импортированные данные поступают с PK, я не использую увеличивающиеся идентификаторы): 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();
}
Я бы предположил, что для решения потребуется мьютекс вокруг всего блока импорта? И если запрос не смог получить мьютекс, он просто переместился бы с остальной частью запроса. Мысли?
РЕДАКТИРОВАТЬ: просто отметить, что это не критический провал. Исключение вылавливается и регистрируется, а затем на запрос отвечает как обычно. И импорт успешно выполняется по другому запросу, а затем на этот запрос отвечает как обычно. Пользователи не являются более умными; они даже не знают об импорте, и это не является основной целью запроса. Так что, действительно, я мог бы просто оставить это как есть, и, кроме случайного исключения, ничего плохого не происходит. Но если есть исправление, позволяющее предотвратить выполнение дополнительной работы или ненужное обращение нескольких запросов к этому другому серверу, это может быть оправдано.
EDIT2: Хорошо, я применил механизм блокировки с flock(). Мысли? Будет ли следующая работа? И как бы я unit test это дополнение?
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;
}