8 Min

Postgres-Transaktionen für mehrstufige Workflows: praktische Muster

Erfahren Sie, wie Sie Postgres-Transaktionen für mehrstufige Workflows nutzen: Updates sicher gruppieren, partielle Schreibvorgänge verhindern, Wiederholungen handhaben und Daten konsistent halten.

Postgres-Transaktionen für mehrstufige Workflows: praktische Muster

Warum mehrstufige Updates oft inkonsistent enden

Die meisten realen Features bestehen nicht aus einem einzigen Datenbank-Update. Es ist eine kurze Kette: eine Zeile einfügen, ein Guthaben aktualisieren, einen Status setzen, einen Audit-Eintrag schreiben, vielleicht einen Job in die Queue stellen. Ein partieller Schreibvorgang tritt auf, wenn nur einige dieser Schritte in die Datenbank gelangen.

Das zeigt sich, wenn etwas die Kette unterbricht: ein Serverfehler, ein Timeout zwischen Ihrer App und Postgres, ein Absturz nach Schritt 2 oder ein Retry, der Schritt 1 erneut ausführt. Jede einzelne Anweisung ist für sich in Ordnung. Der Workflow bricht, wenn er mitten im Ablauf stehen bleibt.

Man erkennt es meist schnell:

  • Eine Zeile existiert, aber die zugehörige Zeile fehlt (Bestellung erstellt, keine Artikel)
  • Geld wurde bewegt, aber der Status wurde nicht geändert (bezahlt, aber weiterhin als unbezahlt markiert)
  • Zwei Datensätze, die eins sein sollten (duplizierte Abonnements nach einem Retry)
  • Flags, die nicht zusammenpassen (Benutzer ist „aktiv“, hat aber keinen Plan)
  • Zustände, die nur unter Last oder bei Fehlern auftreten

Ein konkretes Beispiel: Ein Plan-Upgrade aktualisiert den Plan des Kunden, fügt eine Zahlungszeile hinzu und erhöht verfügbare Credits. Wenn die App nach dem Speichern der Zahlung abstürzt, bevor die Credits hinzugefügt werden, sieht der Support in einer Tabelle „bezahlt“ und in einer anderen „keine Credits“. Wenn der Client erneut sendet, könnten Sie die Zahlung sogar doppelt verbuchen.

Das Ziel ist einfach: Behandeln Sie den Workflow wie einen einzigen Schalter. Entweder gelingt jeder Schritt, oder keiner, damit Sie nie halbfertige Arbeit speichern.

Transaktionen, einfach erklärt

Eine Transaktion ist die Art, wie die Datenbank sagt: Behandle diese Schritte als eine Einheit. Entweder alle Änderungen passieren, oder keine. Das ist wichtig, wann immer Ihr Workflow mehr als ein Update braucht, z. B. eine Zeile anlegen, ein Guthaben aktualisieren und einen Audit-Eintrag schreiben.

Stellen Sie sich vor, Sie überweisen Geld zwischen zwei Konten. Sie müssen bei Konto A abziehen und bei Konto B hinzufügen. Wenn die App nach dem ersten Schritt abstürzt, möchten Sie nicht, dass das System sich nur an die Abbuchung „erinnert".

Commit vs. Rollback

Wenn Sie committen, sagen Sie zu Postgres: behalte alles, was ich in dieser Transaktion gemacht habe. Alle Änderungen werden dauerhaft und für andere Sessions sichtbar.

Wenn Sie rollbacken, sagen Sie zu Postgres: vergiss alles, was ich in dieser Transaktion gemacht habe. Postgres macht die Änderungen rückgängig, als wäre die Transaktion nie passiert.

Was Postgres garantiert (und was nicht)

Innerhalb einer Transaktion garantiert Postgres, dass Sie anderen Sessions keine halb fertigen Ergebnisse zeigen, bevor Sie committen. Wenn etwas fehlschlägt und Sie rollbacken, räumt die Datenbank die Schreibvorgänge dieser Transaktion auf.

Eine Transaktion behebt keine schlechten Workflow-Entscheidungen. Wenn Sie den falschen Betrag abziehen, die falsche Benutzer-ID verwenden oder eine notwendige Prüfung auslassen, wird Postgres das falsche Ergebnis dennoch committen. Transaktionen verhindern auch nicht automatisch jeden geschäftslogischen Konflikt (wie Überverkauf), es sei denn, Sie kombinieren sie mit passenden Constraints, Locks oder dem richtigen Isolationslevel.

Workflows, die zusammengefasst werden sollten

Immer wenn Sie mehr als eine Tabelle (oder mehr als eine Zeile) aktualisieren, um eine einzelne reale Aktion abzuschließen, haben Sie einen Kandidaten für eine Transaktion. Der Punkt bleibt: Entweder ist alles fertig, oder nichts.

Ein Bestellablauf ist der klassische Fall. Sie könnten eine Bestellzeile anlegen, Lagerbestand reservieren, eine Zahlung durchführen und dann den Auftrag als bezahlt markieren. Wenn die Zahlung gelingt, die Statusaktualisierung aber fehlschlägt, haben Sie Geld erfasst, aber die Bestellung sieht weiterhin als unbezahlt aus. Wenn die Bestellzeile erstellt wird, der Bestand aber nicht reserviert wird, können Sie Artikel verkaufen, die Sie gar nicht haben.

Auch das User-Onboarding bricht leise auf dieselbe Weise. Einen Benutzer erstellen, ein Profil einfügen, Rollen zuweisen und aufzeichnen, dass eine Willkommensmail versendet werden soll, ist eine logische Aktion. Ohne Gruppierung können Sie einen Benutzer haben, der sich anmelden kann, aber keine Berechtigungen hat, oder ein Profil, das ohne Benutzer existiert.

Back-Office-Aktionen brauchen oft striktes „Papiertrail + Zustandsänderung“-Verhalten. Eine Anforderung genehmigen, einen Audit-Eintrag schreiben und ein Guthaben aktualisieren sollten zusammen gelingen. Wenn das Guthaben geändert wird, aber der Audit-Log fehlt, verlieren Sie die Nachvollziehbarkeit, wer was warum geändert hat.

Hintergrundjobs profitieren ebenfalls, besonders wenn Sie ein Arbeitselement mit mehreren Schritten verarbeiten: das Element beanspruchen, damit nicht zwei Worker es gleichzeitig tun, das Business-Update anwenden, ein Ergebnis für Reporting und Retries speichern und das Element dann als erledigt markieren (oder mit Grund auf Fehler setzen). Wenn diese Schritte auseinanderlaufen, erzeugen Retries und Konkurrenz Chaos.

Workflow entwerfen, bevor Sie SQL schreiben

Mehrstufige Features brechen, wenn Sie sie wie einen Haufen unabhängiger Updates behandeln. Bevor Sie einen Datenbank-Client öffnen, schreiben Sie den Workflow als Kurzgeschichte mit einem klaren Ziel: Was genau zählt als „fertig" für den Nutzer?

Beginnen Sie damit, die Schritte in einfacher Sprache aufzulisten und dann eine einzige Erfolgsvoraussetzung zu definieren. Zum Beispiel: „Bestellung ist erstellt, Lagerbestand ist reserviert und der Nutzer sieht eine Bestellbestätigungsnummer.“ Alles andere ist kein Erfolg, selbst wenn einige Tabellen aktualisiert wurden.

Zeichnen Sie als Nächstes eine klare Linie zwischen Datenbankarbeit und externer Arbeit. Datenbankschritte sind die, die Sie mit Transaktionen schützen können. Externe Aufrufe wie Kartenabbuchungen, E-Mails versenden oder Drittanbieter-APIs anzusprechen, können langsam und unvorhersehbar fehlschlagen und lassen sich meist nicht zurückrollen.

Eine einfache Planungsmethode: Teile die Schritte in (1) muss alles-oder-nichts sein und (2) kann nach dem Commit passieren.

Entscheiden, was in die Transaktion gehört

Behalten Sie innerhalb der Transaktion nur die Schritte, die konsistent zusammenbleiben müssen:

  • Kern-Records anlegen oder aktualisieren (Order, Invoice, Account Balance)
  • Gemeinsame Ressourcen reservieren (Lagerbestand, Plätze, Kontingente)
  • Ein dauerhaftes „was als Nächstes zu tun ist"-Event aufzeichnen (Outbox-Tabelle)
  • Regeln mit Constraints durchsetzen (Unique Keys, Foreign Keys)

Verschieben Sie Seiteneffekte nach außen. Zum Beispiel: Committen Sie die Bestellung zuerst und senden Sie die Bestätigungs-E-Mail basierend auf einem Outbox-Eintrag.

Für jeden Schritt Rückroll-Erwartungen aufschreiben

Für jeden Schritt schreiben Sie auf, was passieren soll, wenn der nächste Schritt fehlschlägt. „Rollback“ kann eine Datenbank-Rollback bedeuten oder eine kompensierende Aktion.

Beispiel: Wenn die Zahlung gelingt, aber die Lagerreservierung fehlschlägt, entscheiden Sie im Voraus, ob Sie sofort zurückerstatten oder die Bestellung als „Zahlung erfasst, wartet auf Lager“ markieren und asynchron behandeln.

Schritt für Schritt: Einen Workflow in eine Transaktion packen

Eine Transaktion sagt Postgres: Behandle diese Schritte als eine Einheit. Entweder passieren alle, oder keine. Das ist der einfachste Weg, partielle Schreibvorgänge zu verhindern.

Der grundlegende Ablauf

Nutzen Sie eine Datenbankverbindung (eine Session) von Anfang bis Ende. Wenn Sie Schritte über mehrere Verbindungen verteilen, kann Postgres kein Alles-oder-Nichts-Ergebnis garantieren.

Die Reihenfolge ist einfach: BEGIN, führen Sie die erforderlichen Reads und Writes aus, COMMIT wenn alles gelingt, andernfalls ROLLBACK und geben Sie einen klaren Fehler zurück.

Hier ein minimales Beispiel in SQL:

BEGIN;

-- reads that inform your decision
SELECT balance FROM accounts WHERE id = 42 FOR UPDATE;

-- writes that must stay together
UPDATE accounts SET balance = balance - 50 WHERE id = 42;
INSERT INTO ledger(account_id, amount, note) VALUES (42, -50, 'Purchase');

COMMIT;

-- on error (in code), run:
-- ROLLBACK;

Kurz halten (und debuggbar)

Transaktionen halten Locks, solange sie laufen. Je länger Sie sie offenhalten, desto mehr blockieren Sie andere Arbeiten und desto wahrscheinlicher sind Timeouts oder Deadlocks. Tun Sie das Nötigste in der Transaktion und verlagern Sie langsame Aufgaben (E-Mails senden, Zahlungsanbieter anrufen, PDFs erzeugen) nach außen.

Wenn etwas fehlschlägt, protokollieren Sie genügend Kontext, um das Problem reproduzieren zu können, ohne sensible Daten zu leaken: Workflow-Name, order_id oder user_id, Schlüsselparameter (Betrag, Währung) und den Postgres-Fehlercode. Vermeiden Sie das Loggen kompletter Payloads, Kartendaten oder persönlicher Details.

Grundlagen der Konkurrenz: Locks und Isolation ohne Fachchinesisch

Auf Checkout anwenden
Erstellen Sie einen Bestellfluss, der unter Timeouts, Wiederholungen und Konkurrenz korrekt bleibt.

Konkurrenz bedeutet einfach: zwei Dinge passieren zur gleichen Zeit. Stellen Sie sich zwei Kunden vor, die das letzte Konzertticket kaufen. Beide sehen „1 übrig“, beide klicken auf Bezahlen, und jetzt muss Ihre App entscheiden, wer es bekommt.

Ohne Schutz können beide Anfragen denselben alten Wert lesen und beide ein Update schreiben. So entstehen negativer Lagerbestand, doppelte Reservierungen oder eine Zahlung ohne Bestellung.

Row-Locks sind das einfachste Schutzmittel. Sie sperren die konkrete Zeile, die Sie ändern wollen, führen Ihre Prüfungen durch und aktualisieren sie dann. Andere Transaktionen, die dieselbe Zeile anfassen, müssen warten, bis Sie committen oder rollbacken, was doppelte Updates verhindert.

Ein gängiges Muster: Starte eine Transaktion, wähle die Inventarzeile mit FOR UPDATE, verifiziere, dass Bestand vorhanden ist, dekrementiere ihn und füge dann die Bestellung ein. Das „hält die Tür“ zu, während Sie die kritischen Schritte beenden.

Isolationsstufen steuern, wie viele seltsame Überschneidungen von gleichzeitigen Transaktionen Sie zulassen. Der Kompromiss ist meist Sicherheit vs. Geschwindigkeit:

  • Read Committed (Standard): schnell, aber Sie können Änderungen sehen, die andere zwischen Statements committed haben.
  • Repeatable Read: Ihre Transaktion sieht eine stabile Snapshot-Ansicht, gut für konsistente Reads, kann mehr Retries verursachen.
  • Serializable: höchste Sicherheit, Postgres kann eine Transaktion abbrechen, um das Ergebnis so aussehen zu lassen, als wären sie nacheinander gelaufen.

Halten Sie Locks kurz. Wenn eine Transaktion offen bleibt, während Sie eine externe API anrufen oder auf eine Benutzeraktion warten, entstehen lange Wartezeiten und Timeouts. Bevorzugen Sie einen klaren Fehlerpfad: setzen Sie ein Lock-Timeout, fangen Sie den Fehler ab und geben Sie „bitte erneut versuchen“ zurück, statt die Anfragen hängen zu lassen.

Wenn Sie Arbeit außerhalb der Datenbank erledigen müssen (z. B. eine Karte belasten), teilen Sie den Workflow: schnell reservieren, committen, dann den langsamen Teil ausführen und mit einer weiteren kurzen Transaktion finalisieren.

Wiederholungen, die keine Duplikate erzeugen

Retries sind normal in Postgres-basierten Apps. Eine Anfrage kann fehlschlagen, auch wenn Ihr Code korrekt ist: Deadlocks, Statement-Timeouts, kurze Netzwerkabbrüche oder eine Serialisierungsfehler bei höheren Isolationsstufen. Wenn Sie denselben Handler einfach erneut ausführen, riskieren Sie eine zweite Bestellung, doppelte Abbuchungen oder doppelte Event-Zeilen.

Die Lösung ist Idempotenz: die Operation sollte sicher sein, zweimal mit denselben Eingaben ausgeführt zu werden. Die Datenbank sollte erkennen können „das ist dieselbe Anfrage" und konsistent antworten.

Ein praktisches Muster ist, jedem mehrstufigen Workflow einen Idempotency-Key (oft eine client-generierte request_id) beizufügen, ihn auf dem Haupt-Record zu speichern und einen Unique-Constraint auf diesen Key zu setzen.

Beispiel: Beim Checkout erzeugen Sie eine request_id, wenn der Nutzer auf Bezahlen klickt, und fügen die Bestellung mit dieser request_id ein. Wenn ein Retry passiert, schlägt der zweite Versuch am Unique-Constraint fehl und Sie geben die bestehende Bestellung zurück, statt eine neue zu erzeugen.

Was üblicherweise wichtig ist:

  • Verwenden Sie einen Unique-Constraint auf (request_id) oder (user_id, request_id), um Duplikate zu blockieren.
  • Wird der Constraint verletzt, lesen Sie die bestehende Zeile und geben dasselbe Ergebnis zurück.
  • Lassen Sie Seiteneffekte derselben Regel folgen: nur eine Payment-Intent pro Bestellung, nur ein „Order confirmed“-Event pro Bestellung.
  • Loggen Sie die request_id, damit der Support nachvollziehen kann, was passiert ist.

Halten Sie die Retry-Schleife außerhalb der Transaktion. Jeder Versuch sollte eine neue Transaktion starten und die gesamte Einheit von oben neu ausführen. Retries innerhalb einer fehlgeschlagenen Transaktion helfen nicht, weil Postgres sie als aborted markiert.

Ein kleines Beispiel: Ihre App versucht, eine Bestellung zu erzeugen und Inventar zu reservieren, aber es timed out direkt nach COMMIT. Der Client wiederholt. Mit einem Idempotency-Key liefert die zweite Anfrage die bereits erstellte Bestellung zurück und überspringt eine zweite Reservierung, anstatt die Arbeit zu verdoppeln.

Die Datenbank zur Durchsetzung von Regeln nutzen, nicht nur Ihren Code

Mehrstufige Features prototypen
Erstellen Sie einen Checkout- oder Onboarding-Flow mit klaren Commit- und Rollback-Verhalten.

Transaktionen halten einen mehrstufigen Workflow zusammen, aber sie machen die Daten nicht automatisch richtig. Eine starke Strategie gegen partielle Schreibfehler ist, „falsche" Zustände in der Datenbank schwer oder unmöglich zu machen, selbst wenn ein Bug in den Anwendungscode rutscht.

Beginnen Sie mit grundlegenden Sicherheitsgurten. Foreign Keys stellen sicher, dass Referenzen real sind (eine Bestellposition kann nicht auf eine fehlende Bestellung zeigen). NOT NULL verhindert halbgefüllte Zeilen. CHECK-Constraints fangen Werte ab, die keinen Sinn ergeben (z. B. quantity > 0, total_cents >= 0). Diese Regeln laufen bei jedem Write, egal welcher Service oder welches Script die Daten ändert.

Für längere Workflows modellieren Sie Zustandsänderungen explizit. Statt vieler Bool-Flags nutzen Sie eine einzelne Statusspalte (pending, paid, shipped, canceled) und erlauben nur gültige Übergänge. Sie können das mit Constraints oder Triggern erzwingen, sodass die Datenbank illegale Sprünge wie shipped -> pending ablehnt.

Eindeutigkeit ist eine weitere Form von Korrektheit. Fügen Sie Unique-Constraints hinzu, wo Duplikate Ihren Workflow brechen würden: order_number, invoice_number oder ein idempotency_key, der für Retries genutzt wird. Dann blockiert Postgres einen zweiten Insert und Sie können sicher „already processed“ zurückgeben, statt eine zweite Bestellung zu erzeugen.

Wenn Sie Nachvollziehbarkeit brauchen, speichern Sie sie explizit. Eine Audit- oder History-Tabelle, die aufzeichnet, wer was wann geändert hat, macht „mysteriöse Updates" zu Fakten, die Sie bei Incidents abfragen können.

Häufige Fehler, die partielle Schreibvorgänge verursachen

Die meisten partiellen Schreibvorgänge werden nicht durch "schlechtes SQL" verursacht. Sie entstehen durch Workflow-Entscheidungen, die es leicht machen, nur die halbe Geschichte zu committen.

Fallen, die in echten Apps auftauchen

  • "Langsame externe Arbeit in der offenen Transaktion." Einen Zahlungsanbieter anrufen, E-Mails senden oder eine Datei hochladen innerhalb der Transaktion hält Locks länger als nötig. Ist die API langsam oder timeouted, stellen sich andere Nutzer hinter Ihre offene Transaktion an.
  • "Außerhalb der Transaktion lesen und später schreiben." Beispiel: Sie lesen das Guthaben eines Nutzers, zeigen es an und ziehen später basierend auf diesem alten Wert ab. Eine andere Session kann das Guthaben dazwischen geändert haben.
  • "Einen Fehler fangen, aber trotzdem etwas committen." Ein häufiges Muster ist: "versuche Schritt 1, versuche Schritt 2, logge den Fehler, gib trotzdem Erfolg zurück." Wenn der Code nach einem Fehler noch COMMIT erreicht, haben Sie die Datenbank absichtlich inkonsistent gemacht.
  • "Tabellen in unterschiedlicher Reihenfolge in verschiedenen Codepfaden aktualisieren." Wenn eine Anfrage accounts dann orders updated, aber eine andere orders dann accounts, erhöhen Sie die Chance auf Deadlocks unter Last.
  • "Transaktionen zu lange offen halten." Lange Transaktionen können Writes blockieren, das Vacuum verzögern und verwirrende Timeouts erzeugen.

Ein konkretes Beispiel: Beim Checkout reservieren Sie Inventar, erstellen eine Bestellung und belasten dann eine Karte. Wenn Sie die Karte innerhalb derselben Transaktion belasten, halten Sie möglicherweise einen Inventar-Lock, während Sie auf das Netzwerk warten. Gelingt die Belastung, rollbackt Ihre Transaktion später jedoch, dann haben Sie den Kunden belastet, aber keine Bestellung.

Ein sichereres Muster ist: Fokussieren Sie die Transaktion auf den Datenbankzustand (Inventar reservieren, Bestellung anlegen, Zahlung als pending markieren), committen Sie, rufen Sie dann die externe API auf und schreiben Sie das Ergebnis in einer neuen kurzen Transaktion zurück. Viele Teams implementieren das mit einem einfachen pending-Status und einem Hintergrundjob.

Schnelle Checkliste für Alles-oder-Nichts-Operationen

Wenn ein Workflow mehrere Schritte hat (insert, update, charge, send), ist das Ziel einfach: Entweder wird alles aufgezeichnet oder gar nichts.

Transaktionsgrenzen

Behalten Sie alle erforderlichen Datenbankwrites in einer Transaktion. Wenn ein Schritt fehlschlägt, rollbacken Sie und lassen die Daten genau so, wie sie waren.

Machen Sie die Erfolgsvoraussetzung explizit. Zum Beispiel: „Bestellung ist erstellt, Bestand ist reserviert und Zahlungsstatus ist vermerkt." Alles andere ist ein Fehlerpfad, der die Transaktion abbrechen muss.

  • Alle erforderlichen Writes passieren innerhalb eines BEGIN ... COMMIT Blocks.
  • Es gibt einen klaren Done-Zustand in der Datenbank (nicht nur im App-Speicher).
  • Jeder Fehler führt zu ROLLBACK, und der Aufrufer erhält ein klares Fehl­ergebnis.

Sicherheitsgurte (damit Retries nicht schaden)

Gehen Sie davon aus, dass dieselbe Anfrage wiederholt werden kann. Die Datenbank sollte Ihnen helfen, Only-Once-Regeln durchzusetzen.

  • Unterstützen Sie Only-Once-Aktionen mit Unique-Constraints (eine Payment-Zeile pro Bestellung oder eine Reservierung pro Artikel pro Bestellung).
  • Gestalten Sie Retries sicher und wiederholbar ( dieselben Eingaben führen zum selben Endzustand, nicht zu Duplikaten ).

Transaktionen kurz halten

Machen Sie nur das Minimum innerhalb der Transaktion und vermeiden Sie Netzwerkaufrufe, während Locks gehalten werden.

  • Halten Sie Transaktionen kurz und setzen Sie ein Timeout, damit sie nicht hängen.
  • Führen Sie langsame Arbeit (z. B. Zahlungsanbieter) außerhalb der Transaktion aus und schreiben Sie das Ergebnis dann in einer neuen kurzen Transaktion.

Fehler beobachten

Wenn Sie nicht sehen können, wo es bricht, werden Sie weiter raten.

  • Loggen Sie den Workflow-Schritt und eine request id für jeden Fehler.
  • Überwachen Sie Rollback-Raten und Lock-Timeouts, damit Sie partielle Schreibrisiken frühzeitig erkennen.

Beispiel: Ein Checkout-Flow, der bei Fehlern konsistent bleibt

Sicheren Workflow entwerfen
Planen Sie zuerst Ihre Transaktionsgrenzen, dann generieren Sie den React- und Go-Code aus einem Chat.

Ein Checkout hat mehrere Schritte, die zusammen funktionieren sollten: Bestellung anlegen, Lager reservieren, Zahlungsversuch aufzeichnen und dann den Bestellstatus setzen.

Stellen Sie sich vor, ein Nutzer klickt auf Kaufen für 1 Artikel.

Ein sicherer Ablauf (DB-Arbeit ist eine Einheit)

Innerhalb einer Transaktion machen Sie nur Datenbankänderungen:

  • Fügen Sie eine orders-Zeile mit dem Status pending_payment ein.
  • Reservieren Sie Bestand (z. B. inventory.available dekrementieren oder eine reservations-Zeile erstellen).
  • Fügen Sie eine payment_intents-Zeile mit einem client-seitigen idempotency_key (unique) ein.
  • Fügen Sie eine outbox-Zeile wie „order_created" ein.

Wenn eine Anweisung fehlschlägt (kein Bestand, Constraint-Fehler, Absturz), rollt Postgres die ganze Transaktion zurück. Sie haben dann keine Bestellung ohne Reservierung oder eine Reservierung ohne Bestellung.

Was, wenn die Zahlung halb schiefgeht?

Der Payment-Provider liegt außerhalb Ihrer Datenbank, behandeln Sie ihn also als separaten Schritt.

Scheitert der Provider-Aufruf, bevor Sie committen, brechen Sie die Transaktion ab und es wird nichts geschrieben. Scheitert der Provider-Aufruf, nachdem Sie committed haben, starten Sie eine neue Transaktion, die den Zahlungsversuch als fehlgeschlagen markiert, die Reservierung freigibt und den Bestellstatus auf canceled setzt.

Retry ohne zweite Bestellung zu erzeugen

Lassen Sie den Client pro Checkout-Versuch einen idempotency_key senden. Erzwingen Sie ihn mit einem Unique-Index auf payment_intents(idempotency_key) (oder auf orders, wenn Sie das bevorzugen). Bei einem Retry sucht Ihr Code die bestehenden Zeilen und macht weiter, statt eine neue Bestellung einzufügen.

E-Mails und Benachrichtigungen

Senden Sie keine E-Mails innerhalb der Transaktion. Schreiben Sie einen Outbox-Eintrag in derselben Transaktion und lassen Sie einen Hintergrundarbeiter die E-Mail nach dem Commit senden. So versenden Sie nie eine E-Mail für eine Bestellung, die zurückgerollt wurde.

Nächste Schritte: Wenden Sie das diese Woche auf einen Workflow an

Wählen Sie einen Workflow, der mehr als eine Tabelle berührt: Signup + Willkommens-E-Mail enqueuen, Checkout + Inventar, Rechnung + Ledger-Eintrag oder Projekt anlegen + Standard-Einstellungen.

Schreiben Sie zuerst die Schritte, dann die Regeln, die immer gelten müssen (Ihre Invarianten). Beispiel: „Eine Bestellung ist entweder vollständig bezahlt und reserviert, oder nicht bezahlt und nicht reserviert. Nie halb reserviert." Machen Sie diese Regeln zu einer Alles-oder-Nichts-Einheit.

Ein einfacher Plan:

  • Listen Sie die genauen SQL-Operationen in der Reihenfolge auf (Reads, Inserts, Updates, Deletes).
  • Fügen Sie zuerst die fehlenden Datenbank-Constraints hinzu (Unique Keys, Foreign Keys, Check-Constraints).
  • Ergänzen Sie einen Idempotency-Key für die Anfrage, damit Retries keine Duplikate erzeugen.
  • Packen Sie die Schritte in eine Transaktion und machen Sie den Erfolgspunkt explizit (committen Sie nur, wenn alle Prüfungen bestanden sind).
  • Entscheiden Sie, wie ein sicherer Retry aussieht (gleicher Idempotency-Key, gleiches Ergebnis).

Testen Sie dann absichtlich die hässlichen Fälle. Simulieren Sie einen Absturz nach Schritt 2, ein Timeout direkt vor dem Commit und ein Doppelsenden aus der UI. Das Ziel sind langweilige Ergebnisse: keine verwaisten Zeilen, keine doppelten Belastungen, nichts hängt ewig in pending.

Wenn Sie schnell prototypen, hilft es, den Workflow zunächst in einem Planungs-Tool zu skizzieren, bevor Sie Handler und Schema generieren. Zum Beispiel hat Koder.ai (koder.ai) einen Planning Mode und unterstützt Snapshots und Rollback, was beim Iterieren der Transaktionsgrenzen und Constraints nützlich sein kann.

Machen Sie diese eine Woche lang für einen Workflow. Der zweite geht dann viel schneller.

FAQ

Wann sollte ich eine Postgres-Transaktion verwenden?

Verwenden Sie eine Transaktion, wenn eine Benutzeraktion mehrere Zeilen oder Tabellen ändert, deren Daten übereinstimmen müssen. Typische Beispiele sind der Checkout, Kontotransfers, die Registrierung mit Rollenzuweisung und die Reservierung von Warenbeständen.

Was bewirkt COMMIT in Postgres?

Ein Commit macht alle Änderungen der Transaktion dauerhaft und für andere Datenbanksitzungen sichtbar. Führen Sie COMMIT erst aus, wenn alle erforderlichen Prüfungen und Schreibvorgänge erfolgreich waren.

Was passiert bei ROLLBACK?

Ein Rollback bricht die Transaktion ab und verwirft ihre noch nicht festgeschriebenen Änderungen. Ihr Code sollte bei jedem fehlgeschlagenen erforderlichen Schritt ein Rollback durchführen und dann einen Fehler zurückgeben, den der Aufrufer behandeln kann.

Müssen alle Abfragen einer Transaktion dieselbe Verbindung verwenden?

Führen Sie die Datenbank-Lese- und Schreibvorgänge für diesen Ablauf von BEGIN bis COMMIT oder ROLLBACK über dieselbe Datenbankverbindung aus. Getrennte Verbindungen erzeugen getrennte Transaktionen.

Sollte ich Zahlungs- oder E-Mail-APIs innerhalb einer Transaktion aufrufen?

Nein. Transaktionen fassen Datenbankänderungen zusammen, können aber eine Kartenbelastung, eine E-Mail oder einen API-Aufruf eines Drittanbieters nicht rückgängig machen. Schreiben Sie zuerst Ihren Datenbankstatus fest, erfassen Sie ein Outbox-Ereignis oder einen ausstehenden Status und führen Sie die externe Arbeit anschließend getrennt aus.

Wie verhindere ich, dass zwei Nutzer den letzten Artikel kaufen?

Sperren Sie die Zeile, bevor Sie einen gemeinsam genutzten Wert wie den verfügbaren Bestand prüfen und aktualisieren. Wählen Sie beispielsweise die Bestandszeile mit FOR UPDATE aus, prüfen Sie den Bestand, verringern Sie ihn und erstellen Sie die Bestellung, bevor Sie die Transaktion festschreiben.

Wie können Wiederholungsversuche doppelte Bestellungen vermeiden?

Geben Sie jeder Anfrage einen stabilen Idempotenzschlüssel und erzwingen Sie ihn mit einer Unique-Constraint. Wenn ein Wiederholungsversuch gegen diese Constraint stößt, laden Sie das vorhandene Ergebnis und geben es zurück, statt eine weitere Bestellung oder Zahlungsaufzeichnung einzufügen.

Warum müssen Wiederholungsversuche eine neue Transaktion starten?

Wiederholen Sie den Vorgang von Anfang an in einer neuen Transaktion. Nach einem Anweisungsfehler markiert Postgres die aktuelle Transaktion als abgebrochen. Sie kann daher nicht sicher fortgesetzt werden, bis Sie sie zurückgerollt haben.

Welche Datenbank-Constraints helfen, inkonsistente Daten zu verhindern?

Verwenden Sie Fremdschlüssel für gültige Verweise, NOT NULL für Pflichtfelder, CHECK-Constraints für gültige Werte und Unique-Constraints für Datensätze, die nur einmal existieren dürfen. Diese Regeln schützen die Daten auch dann, wenn der Anwendungscode einen Fehler enthält.

Warum sollten Transaktionen kurz bleiben?

Beschränken Sie Transaktionen auf schnelle Datenbankarbeit. Lange Transaktionen halten Sperren, verlängern Wartezeiten und können Deadlocks oder Timeouts verursachen, insbesondere wenn sie auf einen Netzwerkaufruf warten.

Related posts