Also habe ich gerade festgestellt, dass PHP möglicherweise mehrere Anfragen gleichzeitig ausführt. Die Protokolle der letzten Nacht scheinen zu zeigen, dass zwei Anfragen eingegangen sind, die parallel bearbeitet wurden; jeder löste einen Import von Daten von einem anderen Server aus; Jeder versuchte, einen Datensatz in die Datenbank einzufügen. Eine Anfrage schlug fehl, als sie versuchte, einen Datensatz einzufügen, den der andere Thread gerade eingefügt hatte (die importierten Daten kommen mit PKs; ich verwende keine inkrementierenden IDs): SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '865020' for key 'PRIMARY' ...
.PHP Concurrency Problem, mehrere gleichzeitige Anfragen; Mutexe?
- Habe ich dieses Problem richtig diagnostiziert?
- Wie soll ich das angehen?
Folgendes ist ein Teil des Codes. Ich habe viel davon entfernt (die Protokollierung, die Schaffung anderer Entitäten außerhalb des Patienten aus den Daten), aber das Folgende sollte die relevanten Schnipsel enthalten. Anfragen treffen die import() -Methode, die importOne() für jeden zu importierenden Datensatz aufruft. Beachten Sie die Speichermethode in importOne(); Das ist eine Eloquent-Methode (mit Laravel und Eloquent), die das SQL generiert, um den Datensatz je nach Bedarf einzufügen/zu aktualisieren.
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();
}
Ich würde vermuten, dass die Lösung einen Mutex um den gesamten Importblock erfordern würde? Und wenn eine Anfrage keinen Mutex erreichen konnte, würde sie einfach mit dem Rest der Anfrage fortfahren. Gedanken?
EDIT: Nur um zu bemerken, dies ist kein kritischer Fehler. Die Ausnahme wird abgefangen und protokolliert, und dann wird wie gewöhnlich auf die Anforderung geantwortet. Und der Import ist erfolgreich auf der anderen Anfrage, und dann wird diese Anfrage wie üblich beantwortet. Die Benutzer sind nicht klüger; Sie wissen nicht einmal über den Import Bescheid, und das ist nicht der Hauptfokus der Anfrage, die hereinkommt. Also könnte ich das wirklich so lassen, wie es ist, und abgesehen von der gelegentlichen Ausnahme passiert nichts Schlimmes. Aber wenn es einen Fix gibt, der verhindert, dass zusätzliche Arbeit geleistet wird/mehrere Anfragen unnötig an diesen anderen Server gesendet werden, könnte es sich lohnen, diese zu verfolgen.
EDIT2: Okay, ich habe einen Schwung bei der Implementierung eines Sperrmechanismus mit Flock() genommen. Gedanken? Würde die folgende Arbeit? Und wie würde ich diesen Zusatz testen?
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: Gedanken über die folgende alternative Implementierung des Schlosses:
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;
}
Ich habe noch nicht einmal den Inhalt Ihrer Frage gelesen und Ihnen eine Abstimmung gegeben. Gott sei Dank stellt jemand eine echte Frage und repariert nicht nur diesen Tippfehler, wie man eine Zahl rundet, wie man eine Datenbank abfragt! –
Ja, Nebenläufigkeit ist ein Problem. Je nach Situation gibt es viele Möglichkeiten, damit umzugehen. Sperren, optimistisches Sperren, Mutex-Token, Advisory-Locks ... Alles hängt von der besten Lösung für die gegebene Situation ab. Während ich mich auch über eine ernste Frage freue, bin ich mir nicht sicher, ob dies in einer Antwort vernünftig beantwortbar ist ... – deceze
Hast du versucht, deinen eigenen Mutex/Semaphor mit Memcache zu erstellen? Es hilft Ihnen, wenn nur ein Server in die Datenbank schreibt. – lvil