2015-06-03 16 views
27

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?

  1. Habe ich dieses Problem richtig diagnostiziert?
  2. 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; 
} 
+10

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! –

+0

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

+0

Hast du versucht, deinen eigenen Mutex/Semaphor mit Memcache zu erstellen? Es hilft Ihnen, wenn nur ein Server in die Datenbank schreibt. – lvil

Antwort

0

Ich sehe drei Möglichkeiten:
- Verwendung Mutex/Semaphore/some andere Flagge - nicht, um Code einfach und halten
- verwenden DB integrierten Transaktionsmechanismus
- verwenden Sie Warteschlange (wie RabbitMQ oder 0MQ), um Nachrichten in DB hintereinander zu schreiben

+0

Ich denke, ich würde lieber vermeiden, nur Parallelitätsprüfung auf der DB-Ebene zu tun, weil es zu spät ist; zu diesem Zeitpunkt habe ich bereits den anderen Server getroffen, um die Daten zweimal zu importieren. Ich hätte es lieber, wenn der Check früh genug passiert wäre, um zu verhindern, dass die zweite Anfrage irgendeine Arbeit in der import() - Methode erledigt. Es scheint also nur die erste Option zu befriedigen, richtig? – Luke

+0

Wie ich sehe, ist die MQ eine gute Option - legen Sie Ihre Kontrollstrukturen an einem Ort und lassen Sie entscheiden, was eingefügt werden muss. – He11ion

+0

Im Moment kümmert sich ein Standardframework namens Eloquent um die gesamte Datenbankinteraktion. Vielleicht verstehe ich nicht, was Sie vorschlagen, aber es scheint, als wäre es schwierig, sich in eine Warteschlange in diesem Rahmen zu begeben? Und ich denke immer noch, dass die Überprüfung früher erfolgen sollte, so dass die zweite Anfrage an den anderen Server nicht erfolgt. – Luke

5

Es scheint nicht wie y Sie haben eine Racebedingung, weil die ID von der Importdatei kommt und wenn Ihr Importalgorithmus korrekt funktioniert, dann hätte jeder Thread seinen eigenen Shard der Arbeit und sollte niemals mit anderen kollidieren. Jetzt scheint es so zu sein, dass 2 Threads eine Anfrage erhalten, den gleichen Patienten zu erstellen und wegen schlechtem Algorithmus in Konflikt miteinander geraten.

conflictfree

Stellen Sie sicher, dass jeder Thread erzeugt eine neue Zeile aus der Importdatei wird, und nur bei einem Fehler wiederholen.

Wenn Sie das nicht tun können und bei Mutex bleiben möchten, scheint die Verwendung einer Dateisperre keine sehr nette Lösung zu sein, da Sie den Konflikt innerhalb der Anwendung gelöst haben, während er in Ihrer Datenbank tatsächlich auftritt. Ein DB-Lock sollte auch viel schneller und insgesamt eine anständige Lösung sein.

anfordern Datenbanksperre, wie folgt aus:

$ db -> exec ('LOCK TABLES table1 WRITE, table2 WRITE');

Und Sie können einen SQL-Fehler erwarten, wenn Sie in eine gesperrte Tabelle schreiben würden, also umgeben Sie Ihren Patienten-> save() mit einem Versuch fangen.

Eine noch bessere Lösung wäre die Verwendung einer bedingten atomaren Abfrage. Eine DB-Abfrage, in der auch die Bedingung enthalten ist. Sie könnten eine Abfrage wie die folgende verwenden:

+0

Mehrere gleichzeitige Importe ist nicht wünschenswert, ich will nur eine, also keine Notwendigkeit, etwas zu implementieren, um die Arbeit zwischen Threads zu teilen. Was lässt Sie sagen, dass der Konflikt in der Datenbank auftritt? Ich stimme zu, dass die DB-Schicht der Ursprung der Ausnahme ist, aber das liegt daran, dass zwei Threads etwas tun, was sie auf der Anwendungsebene nicht tun sollten. Ist es möglich, DB-Sperren zu verwenden, um zu verhindern, dass sie beide importieren? Ich denke, ich möchte, dass sie die Schreibvorgänge nicht vollständig ausblendet, da andere möglicherweise durch normalen Gebrauch lesen/speichern (nicht importieren). – Luke

+0

Ah, ich habe es gelernt, es waren separate Threads, mein schlechtes. Sie möchten eine Schreibsperre haben, weil Ihre Anwendung den Status liest und dann entsprechend dem zuvor angenommenen Zustand handelt. Während der Zeit zwischen dem Lesen des Status und dem Schreiben des Updates entsprechend dem Status soll der Status in der Zwischenzeit nicht geändert werden. Genau dieser Anwendungsfall, bei dem andere Scripts die gleichen Daten zur gleichen Zeit ändern, wird Ihre Skripte zum Beißen bringen. Bitte beachten Sie auch, dass eine Schreibsperre auf dem Tisch weiterhin Lesevorgänge erlaubt, nur keine Updates/Einfügungen. – RoyB

+1

Ich schlage vor, Sie lesen auf DB-Sperre: beide optimistisch und pessimistisch sperren (AKA lesen und schreiben sperren), Dieser Beitrag gibt eine nette Überschrift: http://StackOverflow.com/Questions/129329/optimistic-VS-Pessimistic-locking Es ist nicht einfach, aber ich glaube, es ist Pflicht für jeden Programmierer zu wissen, in der Lage zu sein, belastbare Software zu schreiben. – RoyB

4

Ihr Beispielcode würde die zweite Anforderung blockieren, bis die erste abgeschlossen ist. Sie müssen LOCK_NB Option für flock() verwenden, um Fehler sofort zurückzugeben und nicht zu warten.

Ja, Sie können entweder Sperren oder Semaphoren verwenden, entweder auf Dateisystemebene oder direkt in der Datenbank.

In Ihrem Fall, wenn Sie jede Importdatei nur einmal verarbeiten müssen, wäre die beste Lösung eine SQL-Tabelle mit einer Zeile für jede Importdatei zu haben. Zu Beginn des Imports fügen Sie die Informationen ein, die der Import ausführt, sodass andere Threads wissen, dass sie ihn nicht erneut verarbeiten. Nachdem der Import abgeschlossen ist, markieren Sie ihn als solchen. (Dann können Sie einige Stunden später die Tabelle überprüfen, ob der Import wirklich abgeschlossen ist.)

Auch ist es besser, solche einmaligen langlebigen Dinge wie Import auf separaten Skripten zu tun und nicht während der normalen Webseiten für Besucher . Zum Beispiel können Sie einen nächtlichen Cron-Job planen, der die Importdatei aufnehmen und verarbeiten würde.

+0

Der Import ist ein Versuch, mit einer anderen DB synchron zu bleiben, muss in Echtzeit sein; daher nicht nächtlich und bei jeder Anfrage:/Danke, ich schaue in LOCK_NB! – Luke

+0

Eigentlich haben Sie mich aufgefordert, in die Datenbank zu sperren. Scheint es machbar, get_lock() und release_lock() zu verwenden? Ich werde meine Implementierung in ein bisschen veröffentlichen ... – Luke

+0

Für Near-Realtime-Synchronisierung (MySQL) Ich schlage kostenlose Tool von Percona Toolset genannt 'Pt-Tabelle-sync' https://www.percona.com/doc/percona- Toolkit/2.2/pt-Tabelle-sync.html. Ich benutze es von cron alle 5 Minuten (kann sogar alle 1 min laufen) – Marki555