PHP-Parallelitätsproblem, mehrere gleichzeitige Anfragen; Mutexe?

Lesezeit: 11 Minuten

Benutzer-Avatar
Lukas

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' ....

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

Benutzer-Avatar
RoyB

Es sieht nicht so aus, als hätten Sie eine Race-Bedingung, da die ID aus der Importdatei stammt und wenn Ihr Importalgorithmus ordnungsgemäß funktioniert, hätte jeder Thread seinen eigenen Teil der zu erledigenden Arbeit und sollte niemals in Konflikt geraten Andere. Jetzt scheint es, als ob 2 Threads eine Anfrage erhalten, denselben Patienten zu erstellen, und wegen eines schlechten Algorithmus miteinander in Konflikt geraten.

konfliktfrei

Stellen Sie sicher, dass jeder generierte Thread eine neue Zeile aus der Importdatei erhält, und wiederholen Sie dies nur bei einem Fehler.

Wenn Sie das nicht können und bei Mutex bleiben wollen, scheint die Verwendung einer Dateisperre keine sehr schöne Lösung zu sein, da Sie den Konflikt jetzt innerhalb der Anwendung gelöst haben, während er tatsächlich in Ihrer Datenbank auftritt. Eine DB-Sperre sollte auch viel schneller und insgesamt eine anständigere Lösung sein.

Fordern Sie eine Datenbanksperre wie folgt an:

$db -> exec(‘TABELLEN SPERREN table1 SCHREIBEN, table2 SCHREIBEN’);

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

Eine noch bessere Lösung wäre die Verwendung einer bedingten atomaren Abfrage. Eine DB-Abfrage, die auch die Bedingung enthält. Sie könnten eine Abfrage wie diese verwenden:

INSERT INTO targetTable(field1) 
SELECT field1
FROM myTable
WHERE NOT(field1 IN (SELECT field1 FROM targetTable))

  • Mehrere gleichzeitige Importe sind nicht wünschenswert, ich möchte nur einen, sodass keine Notwendigkeit besteht, etwas zu implementieren, um die Arbeit zwischen Threads aufzuteilen. Was lässt Sie sagen, dass der Konflikt in der Datenbank auftritt? Ich stimme zu, dass die Ausnahme aus der DB-Schicht stammt, aber das liegt daran, dass zwei Threads etwas tun, was sie auf der Anwendungsschicht nicht tun sollten. Ist es möglich, DB-Sperren zu verwenden, um zu verhindern, dass beide importiert werden? Ich denke, ich möchte, dass sie Schreibvorgänge nicht vollständig blockieren, da andere möglicherweise durch normale Verwendung lesen/speichern (nicht importieren).

    – Lukas

    15. Juni 2015 um 0:28 Uhr

  • Ah, ich dachte, es wären separate Threads, mein Fehler. Sie möchten eine Schreibsperre haben, da Ihre Anwendung den Status liest und dann gemäß dem zuvor angenommenen Status behandelt. In der Zeit zwischen dem Lesen des Status und dem Schreiben Ihres Updates gemäß dem Status möchten Sie nicht, dass sich der Status in der Zwischenzeit ändert. Genau dieser Anwendungsfall, bei dem andere Skripte gleichzeitig dieselben Daten ändern, wird Ihre Skripte dazu bringen, sich gegenseitig zu beißen. Beachten Sie außerdem, dass eine Schreibsperre für die Tabelle weiterhin Lesevorgänge zulässt, nur keine Aktualisierungen/Einfügungen.

    – Roy B

    15. Juni 2015 um 12:26 Uhr


  • Ich schlage vor, dass Sie sich über DB-Sperren informieren: sowohl optimistisches als auch pessimistisches Sperren (auch bekannt als Lese- und Schreibsperre). Programmierer müssen sich dessen bewusst sein, um robuste Software schreiben zu können.

    – Roy B

    15. Juni 2015 um 12:35 Uhr


  • Danke Roy. Würde es Ihnen etwas ausmachen, einen Blick auf das OP zu werfen, ich habe etwas mit Mutexes versucht. Auf App-Ebene und nicht auf DB-Ebene zu tun bedeutet, dass ich nur eine Anfrage an den anderen Server sende, sodass nachfolgende “kollidierende” Anfragen an den Hauptserver so schnell wie möglich beantwortet werden können. Gedanken?

    – Lukas

    17. Juni 2015 um 2:19 Uhr

  • Die Sperre scheint auf die richtige Weise implementiert zu sein, beachten Sie jedoch, dass Sie damit keine Tabellensperre erhalten. Daher ist jede andere Anwendung, die mit der DB kommuniziert, nicht eingeschränkt, wie dies bei einer Tabellensperre der Fall wäre. Andere Clients der DB können Ihr benanntes Schloss “umgehen”, indem sie es einfach nicht überprüfen. Wenn Sie das Ganze in einer Anfrage erledigen möchten, sollten Sie nicht alle diese Entitäten save aufrufen und eine große Abfrage aufbauen. Jetzt scheint es mir, dass jede save(); würde eine Anfrage senden. Auch wenn Ihre MySQL-Verbindung geschlossen wird, wird Ihre Sperre automatisch aufgehoben.

    – Roy B

    17. Juni 2015 um 13:59 Uhr


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

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

In Ihrem Fall, in dem jede Importdatei nur einmal verarbeitet werden muss, wäre die beste Lösung, eine SQL-Tabelle mit Zeilen für jede Importdatei zu haben. Zu Beginn des Imports fügen Sie die Information ein, dass der Import im Gange ist, damit andere Threads wissen, dass sie ihn nicht erneut verarbeiten sollen. Nachdem der Import abgeschlossen ist, markieren Sie ihn als solchen. (Dann können Sie einige Stunden später in der Tabelle nachsehen, ob der Import wirklich abgeschlossen ist.)

Außerdem ist es besser, solche einmaligen, langlebigen Dinge wie den Import in separaten Skripten durchzuführen und nicht, während den Besuchern normale Webseiten bereitgestellt werden. Beispielsweise können Sie einen nächtlichen Cron-Job planen, der die Importdatei abholt und verarbeitet.

  • Der Import ist ein Versuch, mit einer anderen DB synchron zu bleiben, muss nahezu in Echtzeit erfolgen; daher nicht jede Nacht und bei jeder Anfrage :/ Danke, ich werde in LOCK_NB nachsehen!

    – Lukas

    16. Juni 2015 um 1:54 Uhr

  • Eigentlich haben Sie mich aufgefordert, mich mit Sperren in der Datenbank zu befassen. Scheint es möglich, get_lock() und release_lock() zu verwenden? Ich werde meine Implementierung in Kürze posten …

    – Lukas

    16. Juni 2015 um 1:55 Uhr

  • Für die Synchronisierung nahezu in Echtzeit (MySQL) schlage ich ein kostenloses Tool aus dem Percona-Toolset namens vor pt-table-sync percona.com/doc/percona-toolkit/2.2/pt-table-sync.html . Ich benutze es alle 5 Minuten von Cron (kann sogar alle 1 Minute ausgeführt werden)

    – Marki555

    16. Juni 2015 um 8:12 Uhr

  • Klingt nach einem nützlichen Tool! Ich repliziere jedoch keine Daten; Abrufen von Daten, Umstrukturieren und Hinzufügen verschiedener Felder und Entfernen von Überschüssen und so weiter für eine kleine Hilfsanwendung. Es ist ein Prototypstück. Ich habe auch keine Kontrolle über diesen anderen Server; Ich bekomme irgendwie, was mir gegeben wird. Trotzdem danke!

    – Lukas

    17. Juni 2015 um 2:18 Uhr

Ich sehe drei Möglichkeiten:

– Mutex/Semaphor/ein anderes Flag verwenden – nicht einfach zu codieren und zu warten

– DB-integrierten Transaktionsmechanismus verwenden

– Verwenden Sie eine Warteschlange (wie RabbitMQ oder 0MQ), um Nachrichten nacheinander in die DB zu schreiben

  • Ich denke, ich würde es lieber vermeiden, nur eine Parallelitätsprüfung auf DB-Ebene durchzuführen, da es zu spät ist. Zu diesem Zeitpunkt habe ich bereits den anderen Server aufgerufen, um die Daten zweimal zu importieren. Ich möchte lieber, dass die Überprüfung früh genug erfolgt, um zu verhindern, dass die zweite Anforderung in der Methode import() funktioniert. Also scheint nur die erste Option das zu erfüllen, richtig?

    – Lukas

    3. Juni 2015 um 7:24 Uhr

  • Wie ich sehe, ist der MQ eine gute Option – platzieren Sie Ihre Kontrollstrukturen an einem Ort und lassen Sie ihn entscheiden, was eingefügt werden muss.

    – He11ion

    3. Juni 2015 um 7:38 Uhr

  • Im Moment kümmert sich ein Standard-Framework 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 dieses Framework einzureihen? Und ich bin immer noch der Meinung, dass die Überprüfung früher erfolgen sollte, sodass die zweite Anfrage an den anderen Server nicht erfolgt.

    – Lukas

    5. Juni 2015 um 0:10 Uhr

  • Dies ist keine vollständige und nützliche Antwort … Bitte verwenden Sie Kommentare, wenn Sie Lösungsanweisungen vorschlagen, und senden Sie eine Antwort, wenn Sie eine vollständige Antwort / Lösung auf eine Frage haben.

    – Roy B

    12. Juni 2015 um 15:07 Uhr

1121510cookie-checkPHP-Parallelitätsproblem, mehrere gleichzeitige Anfragen; Mutexe?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy