Im Laufe Ihrer Karriere werden Sie auf Schrödingers Katzenprobleme stoßen, Situationen, die manchmal funktionieren und manchmal nicht. Die Rennbedingungen sind eine dieser Herausforderungen (ja, nur eine!).
In diesem Blog-Beitrag werde ich ein Beispiel aus der Praxis vorstellen, zeigen, wie das Problem reproduziert werden kann, und Strategien für den Umgang mit Race Conditions mithilfe serialisierbarer Transaktionsisolation und Empfehlungssperren mit PostgreSQL diskutieren.
Inspiriert von „Entwerfen datenintensiver Anwendungen“, Kapitel 7 – Transaktionen „Schwache Isolationsstufen“
Github Repository mit Praxisbeispiel
Diese Anwendung verwaltet Bereitschaftsschichten für Ärzte in einem Krankenhaus. Um uns auf das Race-Condition-Problem zu konzentrieren, vereinfachen wir unser Szenario. Unsere App wird um diese einzelne Tabelle herum aufgelöst:
CREATE TABLE shifts ( id SERIAL PRIMARY KEY, doctor_name TEXT NOT NULL, shift_id INTEGER NOT NULL, on_call BOOLEAN NOT NULL DEFAULT FALSE );
Wir haben eine wichtige Geschäftsregel:
Wie Sie vielleicht schon vermutet haben, kann die Implementierung einer naiven API zu Race-Condition-Szenarien führen. Betrachten Sie diese hypothetische Situation:
Jack und John sind beide während derselben Schicht im Krankenhaus auf Abruf. Fast gleichzeitig beschließen sie, einen Urlaubsantrag zu stellen. Der eine hat Erfolg, der andere verlässt sich auf veraltete Informationen darüber, wie viele Ärzte im Dienst sind. Infolgedessen verlassen beide ihre Schicht, verstoßen gegen die Geschäftsregeln und verlassen eine bestimmte Schicht, ohne dass Ärzte auf Abruf sind:
John --BEGIN------doctors on call: 2-------leave on call-----COMMIT--------> (t) \ \ \ \ \ \ \ \ Database ------------------------------------------------------------------> (t) / / / / / / / / Jack ------BEGIN------doctors on call: 2-----leave on call----COMMIT-------> (t)
Die Anwendung ist eine einfache, in Golang implementierte API. Schauen Sie sich das GitHub-Repository an, um Anweisungen zum Ausführen und Ausführen des Skripts zur Reproduktion dieses Race-Condition-Szenarios zu erhalten. Zusammenfassend müssen Sie Folgendes tun:
Der Test versucht, zwei Ärzte gleichzeitig abzurufen und erreicht Endpunkte mit unterschiedlichen Ansätzen: shiftId=1 verwendet Beratungssperre, shiftId=2 verwendet serialisierbare Transaktionsisolation und shiftId=3 ist eine naive Implementierung ohne Parallelitätskontrolle.
Die k6-Ergebnisse geben benutzerdefinierte Metriken aus, um anzuzeigen, welche Schicht-ID gegen die Geschäftsregel verstoßen hat:
✓ at least one doctor on call for shiftId=1 ✓ at least one doctor on call for shiftId=2 ✗ at least one doctor on call for shiftId=3 ↳ 36% — ✓ 123 / ✗ 217
Sie benötigen Tools wie Yarn, Go, K6 und Docker, oder Sie können DevBox verwenden, um Repository-Abhängigkeiten einfacher einzurichten.
Das Problem tritt auf, wenn unsere Anwendung eine Entscheidung auf der Grundlage veralteter Daten trifft. Dies kann passieren, wenn zwei Transaktionen fast gleichzeitig laufen und beide versuchen, Ärzte für ihre Schicht abzurufen. Eine Transaktion ist wie erwartet erfolgreich, aber die andere, die sich auf veraltete Informationen stützt, ist ebenfalls fehlerhaft. Wie können wir dieses unerwünschte Verhalten verhindern? Es gibt mehrere Möglichkeiten, dies zu erreichen, und ich werde zwei von PostgreSQL unterstützte Optionen untersuchen, obwohl ähnliche Lösungen in anderen Datenbankverwaltungssystemen zu finden sind.
Serialisierbare Snapshot-Isolation erkennt und verhindert automatisch Anomalien wie den von unserer Anwendung gezeigten Schreibversatz.
Ich werde nicht näher auf die Theorie hinter der Transaktionsisolation eingehen, aber sie ist ein häufiges Thema in vielen gängigen Datenbankverwaltungssystemen. Sie können gute Materialien finden, indem Sie nach Snapshot-Isolation suchen, wie dieses in der offiziellen PostgreSQL-Dokumentation zur Transaktionsisolation. Darüber hinaus finden Sie hier das Papier, in dem diese Lösung vor Jahren vorgeschlagen wurde. Reden ist billig, also schauen wir uns den Code an:
Starten Sie zunächst die Transaktion und setzen Sie die Isolationsstufe auf Serialisierbar:
// Init transaction with serializable isolation level tx, err := db.BeginTxx(c.Request().Context(), &sql.TxOptions{ Isolation: sql.LevelSerializable, })
Fahren Sie dann mit der Ausführung der Vorgänge fort. In unserem Fall wird diese Funktion ausgeführt:
CREATE OR REPLACE FUNCTION update_on_call_status_with_serializable_isolation(shift_id_to_update INT, doctor_name_to_update TEXT, on_call_to_update BOOLEAN) RETURNS VOID AS $$ DECLARE on_call_count INT; BEGIN -- Check the current number of doctors on call for this shift SELECT COUNT(*) INTO on_call_count FROM shifts s WHERE s.shift_id = shift_id_to_update AND s.on_call = TRUE; IF on_call_to_update = FALSE AND on_call_count = 1 THEN RAISE EXCEPTION '[SerializableIsolation] Cannot set on_call to FALSE. At least one doctor must be on call for this shiftId: %', shift_id_to_update; ELSE UPDATE shifts s SET on_call = on_call_to_update WHERE s.shift_id = shift_id_to_update AND s.doctor_name = doctor_name_to_update; END IF; END; $$ LANGUAGE plpgsql;
Wenn aufgrund der gleichzeitigen Ausführung inkonsistente Szenarien auftreten, ermöglicht die serialisierbare Isolationsstufe den Erfolg einer Transaktion und setzt die anderen automatisch mit dieser Meldung zurück, sodass Sie es sicher erneut versuchen können:
ERROR: could not serialize access due to read/write dependencies among transactions
Eine weitere Möglichkeit, die Durchsetzung unserer Geschäftsregeln sicherzustellen, besteht darin, die Ressource explizit für eine bestimmte Schicht zu sperren. Dies können wir durch eine Advisory Lock auf Transaktionsebene erreichen. Diese Art von Sperre wird vollständig von der Anwendung gesteuert. Weitere Informationen dazu finden Sie hier.
Es ist wichtig zu beachten, dass Sperren sowohl auf Sitzungs- als auch auf Transaktionsebene angewendet werden können. Hier können Sie die verschiedenen verfügbaren Funktionen erkunden. In unserem Fall verwenden wir pg_try_advisory_xact_lock(key bigint) → boolean, wodurch die Sperre nach einem Commit oder Rollback automatisch aufgehoben wird:
BEGIN; -- Attempt to acquire advisory lock and handle failure with EXCEPTION IF NOT pg_try_advisory_xact_lock(shift_id_to_update) THEN RAISE EXCEPTION '[AdvisoryLock] Could not acquire advisory lock for shift_id: %', shift_id_to_update; END IF; -- Perform necessary operations -- Commit will automatically release the lock COMMIT;
Hier ist die vollständige Funktion, die in unserer Anwendung verwendet wird:
-- Function to Manage On Call Status with Advisory Locks, automatic release when the trx commits CREATE OR REPLACE FUNCTION update_on_call_status_with_advisory_lock(shift_id_to_update INT, doctor_name_to_update TEXT, on_call_to_update BOOLEAN) RETURNS VOID AS $$ DECLARE on_call_count INT; BEGIN -- Attempt to acquire advisory lock and handle failure with NOTICE IF NOT pg_try_advisory_xact_lock(shift_id_to_update) THEN RAISE EXCEPTION '[AdvisoryLock] Could not acquire advisory lock for shift_id: %', shift_id_to_update; END IF; -- Check the current number of doctors on call for this shift SELECT COUNT(*) INTO on_call_count FROM shifts s WHERE s.shift_id = shift_id_to_update AND s.on_call = TRUE; IF on_call_to_update = FALSE AND on_call_count = 1 THEN RAISE EXCEPTION '[AdvisoryLock] Cannot set on_call to FALSE. At least one doctor must be on call for this shiftId: %', shift_id_to_update; ELSE UPDATE shifts s SET on_call = on_call_to_update WHERE s.shift_id = shift_id_to_update AND s.doctor_name = doctor_name_to_update; END IF; END; $$ LANGUAGE plpgsql;
Dealing with race conditions, like the write skew scenario we talked about, can be pretty tricky. There's a ton of research and different ways to solve these problems, so definitely check out some papers and articles if you're curious.
These issues can pop up in real-life situations, like when multiple people try to book the same seat at an event or buy the same spot in a theater. They tend to appear randomly and can be hard to figure out, especially if it's your first time dealing with them.
When you run into race conditions, it's important to look into what solution works best for your specific situation. I might do a benchmark in the future to compare different approaches and give you more insights.
I hope this post has been helpful. Remember, there are tools out there to help with these problems, and you're not alone in facing them!
Das obige ist der detaillierte Inhalt vonUmgang mit Race Conditions: Ein praktisches Beispiel. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!