Ich habe gerade festgestellt, dass PHP möglicherweise mehrere Anfragen gleichzeitig ausführt. Die Protokolle von letzter Nacht scheinen zu zeigen, dass zwei Anfragen eingegangen sind, die parallel bearbeitet wurden; jeder löste einen Datenimport von einem anderen Server aus; Jeder versuchte, einen Datensatz in die Datenbank einzufügen. Eine Anfrage schlug fehl, als versucht wurde, einen Datensatz einzufügen, den der andere Thread gerade eingefügt hatte (die importierten Daten werden mit PKs geliefert; ich verwende keine inkrementierenden IDs): SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '865020' for key 'PRIMARY' ...
.
- Habe ich dieses Problem richtig diagnostiziert?
- Wie soll ich das ansprechen?
Das Folgende ist ein Teil des Codes. Ich habe viel davon entfernt (die Protokollierung, die Erstellung anderer Entitäten außerhalb des Patienten aus den Daten), aber das Folgende sollte die relevanten Ausschnitte enthalten. Anfragen treffen im Wesentlichen auf die import()-Methode, die importOne() für jeden zu importierenden Datensatz aufruft. Beachten Sie die save-Methode in importOne(); das ist eine Eloquent-Methode (unter Verwendung von Laravel und Eloquent), die die SQL generiert, um den Datensatz entsprechend einzufügen/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 anzumerken, dass dies kein kritischer Fehler ist. Die Ausnahme wird abgefangen und protokolliert, und dann wird wie gewohnt auf die Anfrage geantwortet. Und der Import ist bei der anderen Anfrage erfolgreich, und dann wird diese Anfrage wie gewohnt beantwortet. Die Benutzer sind nicht klüger; Sie wissen nicht einmal über den Import Bescheid, und das ist nicht der Hauptfokus der eingehenden Anfrage. Also wirklich, ich könnte das einfach so laufen lassen, wie es ist, und abgesehen von gelegentlichen Ausnahmen passiert nichts Schlimmes. Aber wenn es einen Fix gibt, um zu verhindern, dass unnötigerweise zusätzliche Arbeit geleistet wird/mehrere Anfragen an diesen anderen Server gesendet werden, könnte es sich lohnen, dies zu verfolgen.
EDIT2: Okay, ich habe versucht, einen Sperrmechanismus mit flock() zu implementieren. Gedanken? Würde folgendes funktionieren? 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 zur folgenden alternativen Implementierung der Sperre:
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 den Inhalt Ihrer Frage nicht einmal gelesen und Ihnen eine positive Stimme gegeben. Gott sei Dank stellt jemand eine echte Frage und behebt nicht nur diesen Tippfehler, wie man eine Zahl rundet, wie man eine Datenbank abfragt!
– Hanky Panky
3. Juni 2015 um 6:51 Uhr
Ja, Nebenläufigkeit ist ein Problem. Je nach Situation gibt es viele Möglichkeiten, damit umzugehen. Locking, Optimistic Locking, 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 ernsthafte Frage freue, bin ich mir nicht sicher, ob dies vernünftigerweise in einer Antwort beantwortet werden kann …
– verzeihen
♦
3. Juni 2015 um 7:15 Uhr
Haben Sie versucht, Ihren eigenen Mutex/Semaphor mit Memcache zu erstellen? Es hilft Ihnen, wenn nur ein Server in die Datenbank schreibt.
– böse
3. Juni 2015 um 7:20 Uhr
Schrieb einen Mutex-ähnlichen Mechanismus mit flock() … scheint das vernünftig zu sein? Siehe OP für die Bearbeitung. Irgendeine Idee, wie ich das Unit testen würde? Wie drücke ich die Methode import () zweimal gleichzeitig …?
– Lukas
5. Juni 2015 um 0:44 Uhr
Nur ein Hinweis für alle, die daran denken, eine Dateisperre zu verwenden; es verursacht lustige Kopfschmerzen, wenn Sie die Berechtigungen des Dateisystems auf dem Server nicht kontrollieren. /s
– Lukas
9. Juni 2015 um 9:50 Uhr