Ein Web-Backend wirkt oft deterministisch: Request rein, Business-Logik läuft, Datenbank schreibt, Response raus. Unter paralleler Last ist das eine Illusion. Zwei Requests können denselben Datensatz lesen, beide „korrekt“ rechnen und am Ende widersprüchlich schreiben. Genau hier entscheidet die Datenbank über Korrektheit: über Transaktionen, Sperren und die passende Isolationsstufe.
In der Praxis geht es selten um akademische Theorie, sondern um ganz konkrete Fehlerbilder: Warenkörbe, die plötzlich leer sind, Reservierungen, die doppelt vergeben werden, oder Admin-UIs, die Änderungen „verschlucken“. Wer diese Probleme nachhaltig lösen will, braucht drei Bausteine: ein Modell der Konflikte, eine saubere Transaktionsgrenze in der Anwendung und eine bewusste Wahl der Isolation.
Welche Nebenläufigkeitsfehler in Backends wirklich auftreten
Lost Update: zwei Writes, einer verschwindet
Klassiker bei Profil-Updates oder Zählerständen: Request A und B laden denselben Datensatz, ändern unterschiedliche Felder und schreiben jeweils das ganze Objekt zurück. Wenn die Anwendung nicht differenziell schreibt (nur geänderte Felder) oder kein Konflikt erkannt wird, überschreibt der zweite Write den ersten. Das wirkt wie „die Datenbank hat etwas verloren“, ist aber ein Anwendungsmuster ohne Schutz.
Dirty Read und Non-Repeatable Read: inkonsistente Zwischenstände
Ein Dirty Read passiert, wenn ein Request Daten liest, die ein anderer Request zwar geschrieben, aber noch nicht committet hat. Rollt der schreibende Request zurück, hat der lesende Request etwas gesehen, das nie existiert hat. Non-Repeatable Reads sind subtiler: derselbe SELECT in derselben logischen Operation liefert unterschiedliche Werte, weil ein anderer Request dazwischen committet. Beides kann Reports verfälschen oder in Workflows zu falschen Entscheidungen führen.
Phantoms: „neue“ Zeilen tauchen in derselben Operation auf
Phantom Reads entstehen typischerweise bei Bereichsabfragen, z. B. „alle freien Slots am Tag X“ oder „alle offenen Bestellungen eines Kunden“. Wird zwischen zwei Abfragen eine passende Zeile eingefügt oder der Status geändert, verändert sich die Menge. Das ist besonders relevant, wenn aus einer Abfrage heraus Entscheidungen abgeleitet werden (z. B. Kapazität prüfen und dann anlegen).
Isolationsstufen: was sie zusichern und was nicht
Read Committed als Default: solide, aber nicht konfliktfrei
Viele Systeme starten mit Read Committed, weil es ein gutes Verhältnis aus Parallelität und Verständlichkeit bietet: gelesene Daten sind committet, Dirty Reads werden vermieden. Trotzdem bleiben Lost Updates, Non-Repeatable Reads und Phantoms je nach Datenbank und Zugriffsmuster möglich. Das ist nicht „schlecht“, aber es bedeutet: Korrektheit muss zusätzlich durch Sperren oder konfliktbewusste Writes hergestellt werden.
Repeatable Read: stabilere Reads, aber Range-Probleme bleiben möglich
Repeatable Read sorgt typischerweise dafür, dass innerhalb einer Transaktion dieselbe Zeile bei wiederholtem Lesen stabil bleibt. Das hilft bei Workflows, die mehrere Schritte über denselben Datensatz machen. Bei Bereichsabfragen können aber weiterhin Phantoms auftreten (abhängig vom Datenbank-Implementierungsmodell), und Schreibkonflikte sind nicht automatisch gelöst. Wichtig ist, die reale Semantik der eingesetzten Datenbank zu kennen, statt sich nur auf Namen zu verlassen.
Serializable: maximale Korrektheit, aber mit Kosten
Serializable ist die stärkste Isolation: das Ergebnis soll so wirken, als liefen Transaktionen nacheinander. In der Praxis kann das über Sperren oder über Konflikterkennung mit Rollbacks umgesetzt werden. Beides kostet: entweder warten Requests häufiger, oder es gibt mehr Abbrüche, die die Anwendung sauber retryen muss. Serializable ist sinnvoll, wenn fachliche Korrektheit Vorrang hat, etwa bei Geldbewegungen oder knappen Kontingenten.
Transaktionsgrenzen in Services sauber ziehen
Eine Transaktion pro fachlichem Schritt, nicht pro HTTP-Request
Ein häufiger Fehler ist, „alles im Request“ in eine Transaktion zu packen, inklusive externer API-Calls oder langsamer Dateizugriffe. Dadurch werden Locks länger gehalten als nötig, was Latenzspitzen und Deadlocks begünstigt. Besser ist eine Transaktion, die genau den kritischen Datenbankteil umfasst: lesen (falls nötig), prüfen, schreiben, committen. Externe Calls gehören davor oder danach, je nach Semantik, oft abgesichert über asynchrone Verarbeitung.
Die Reihenfolge der Zugriffe bewusst stabil halten
Deadlocks entstehen nicht nur durch „zu viele Locks“, sondern durch unterschiedliche Reihenfolgen. Wenn zwei Codepfade Datensätze in unterschiedlicher Reihenfolge sperren (z. B. erst Konto, dann Bestellung vs. erst Bestellung, dann Konto), kann es zyklisch werden. In Teams hilft eine Konvention: kritische Ressourcen immer in derselben Reihenfolge sperren, und Updates auf mehrere Tabellen klar strukturieren (z. B. erst Parent, dann Children).
Retries sind Teil des Designs, nicht Workaround
Bei stärkerer Isolation oder bei explizitem Sperren kann es zu Konflikten kommen: Lock-Timeouts, Deadlocks oder serialisierbare Abbrüche. Das ist kein Ausnahmefall, sondern unter Last erwartbar. Retries müssen kontrolliert passieren: begrenzt, mit Backoff (kurze Wartezeit) und nur für eindeutig transiente Fehler. Bei Endlosschleifen oder unkontrollierten Retries wird aus einem Konflikt schnell eine Lastspirale.
Konkrete Muster für typische Backend-Szenarien
Bestandsreservierung: „prüfen und dann schreiben“ ohne Rennen
Das Muster „SELECT Bestand, wenn > 0 dann UPDATE“ ist anfällig für Rennen. Stabiler ist ein atomischer Write, der die Bedingung im UPDATE/INSERT selbst ausdrückt. Beispiel: ein UPDATE, das nur dann reduziert, wenn noch genug Bestand vorhanden ist. Anschließend wird geprüft, wie viele Zeilen betroffen waren. So entsteht Korrektheit über eine einzelne Schreiboperation, die die Datenbank sauber serialisiert.
Wenn zusätzlich eine Reservierungstabelle gepflegt wird (z. B. Reservierungen laufen nach 15 Minuten ab), sollte die Transaktion die Bestandsänderung und das Anlegen der Reservierung umfassen. Lange Nacharbeiten (E-Mail, Payment-Provider) dürfen nicht in derselben Transaktion laufen.
Geldbewegungen: Ledger statt „Saldo überschreiben“
Saldo-Felder sind bequem, aber riskant, wenn mehrere Buchungen parallel eintreffen. Robuster ist ein Ledger-Ansatz: jede Bewegung wird als eigene Zeile geschrieben, der Saldo ergibt sich aus Aggregation oder aus einem separat gepflegten, konsistent aktualisierten Snapshot. Der zentrale Punkt ist die eindeutige Reihenfolge und die Vermeidung von „Overwrite“-Writes. Bei Bedarf kann eine per Konto serialisierte Sperrstrategie helfen, wenn jede Buchung zwingend auf dem aktuellsten Saldo aufbauen muss.
„Nur ein aktiver Datensatz“: eindeutige Constraints statt Logik
Häufige Regel: pro Nutzer darf es nur ein aktives Abo geben, pro Gerät nur ein Token, pro E-Mail nur ein Konto. Diese Regel ausschließlich in Anwendungscode zu prüfen, ist unter Parallelität fragil. Die Datenbank sollte die Invariante erzwingen: eindeutige Indizes/Constraints auf den relevanten Schlüsseln. Die Anwendung behandelt dann Constraint-Verletzungen als fachlichen Konflikt und reagiert entsprechend (z. B. 409 Conflict). Das reduziert die Abhängigkeit von hoher Isolation.
Was beim Sperren in der Praxis zählt
Pessimistisch vs. optimistisch: an Konfliktrate orientieren
Pessimistisches Sperren blockiert andere Writer früh (z. B. „select for update“). Das lohnt sich, wenn Konflikte häufig sind und ein späterer Rollback teuer wäre. Optimistische Verfahren arbeiten ohne frühe Locks und erkennen Konflikte beim Schreiben, typischerweise über eine Version-Spalte oder Timestamps. Das ist effizient, wenn Konflikte selten sind und die Wiederholung günstig bleibt.
Für viele CRUD-lastige Bereiche (Profile, Stammdaten) passt optimistisch gut. Für knappe Ressourcen (Tickets, Lagerbestand, Nummernkreise) ist pessimistisches Sperren oder ein atomischer Write oft einfacher zu begründen.
Lock-Dauer minimieren: kleine Transaktionen, gezielte Queries
Locks werden nicht „böse“, sie werden nur zu lange gehalten. Das passiert durch unnötige Reads, zu große Resultsets oder durch das Vermischen mehrerer fachlicher Schritte. Hilfreich sind: nur benötigte Spalten laden, Updates gezielt formulieren, und keine langen Schleifen innerhalb einer Transaktion. Auch die Wahl eines passenden Index kann Locking indirekt verbessern, weil weniger Zeilen berührt werden.
Entscheidungshilfe für Isolation und Konfliktstrategie
Die folgenden Schritte helfen, ohne Dogma eine sinnvolle Einstellung pro Use-Case zu wählen. Entscheidend ist, ob fachliche Invarianten wirklich „hart“ sind (dürfen nie verletzt werden) oder ob kurzfristige Inkonsistenz tolerierbar ist (z. B. bei Listenansichten).
- Fachliche Invarianten identifizieren: Was darf nie passieren (z. B. negativer Bestand, doppelte Reservierung, zwei aktive Abos)?
- Invarianten möglichst durch Datenbankregeln absichern: Constraints und eindeutige Indizes, statt nur if-Checks im Code.
- Schreiboperationen bevorzugt atomisch formulieren: Bedingung ins UPDATE/INSERT ziehen, betroffene Zeilen auswerten.
- Bei häufigen Konflikten gezielt sperren: kurze Transaktion, klare Lock-Reihenfolge, Timeout- und Retry-Strategie.
- Isolation nur erhöhen, wenn das Problem nicht anders sauber lösbar ist: höhere Isolation bedeutet mehr Wartesituationen oder mehr Abbrüche.
- Konfliktpfade testen: Parallelitätstests mit zwei gleichzeitigen Requests, um Lost Updates und Double-Spends reproduzierbar zu machen.
Integration ins Backend: Tests, Observability und sichere Defaults
Parallelität testen: nicht nur Unit-, auch Integrationsniveau
Viele Nebenläufigkeitsfehler sind auf Unit-Test-Ebene unsichtbar. Sinnvoll sind Integrationstests, die zwei Transaktionen parallel ausführen und gezielt auf Rennen testen. Praktisch ist ein Test, der zwei gleichzeitige Reservierungen startet und sicherstellt, dass genau eine gewinnt. Auf der Service-Ebene sollten Konflikte als klarer Fehlerfall modelliert sein: entweder als 409/422 oder als definierte Domänenfehlermeldung.
Messbarkeit: Lock-Wait und Deadlocks im Blick behalten
Wenn Isolation oder Sperrstrategie verändert wird, muss im Betrieb sichtbar sein, ob Requests warten. Hilfreich sind Metriken für Latenz nach Endpoint, Fehlerraten für transiente DB-Fehler sowie Logs, die Deadlocks und Lock-Timeouts eindeutig klassifizieren. Für den Zusammenhang zwischen Request und Datenbankaktivität kann verteiltes Tracing helfen, etwa über Distributed Tracing im Backend.
Umfeldthemen: Timeouts, Retries und Idempotenz
Eine saubere Konfliktstrategie hängt an angrenzenden Themen. Wenn Datenbankoperationen länger warten, müssen Request-Timeouts sinnvoll gesetzt sein, damit Clients nicht „blind“ wiederholen. Dazu passt eine explizite Retry-Policy in Worker/Jobs statt im Frontend. Für wiederholte Requests ist außerdem wichtig, dass schreibende Endpoints idempotent ausgelegt sind, damit Retries nicht doppelt buchen. Vertiefend helfen Request-Timeouts im Backend und idempotente APIs.
Typische Stolperfallen beim Einsatz in ORMs
„Autocommit“ und versteckte Transaktionen
Viele ORMs öffnen implizit Transaktionen oder verlassen sich auf Autocommit. Das kann im Happy Path funktionieren, macht aber Konflikte schwer reproduzierbar, weil nicht klar ist, wo eine Transaktion beginnt und endet. Für kritische Use-Cases sollte die Anwendung die Transaktionsgrenze explizit setzen und klar dokumentieren, welche Queries darin laufen.
Lazy Loading kann Locks verlängern
Lazy Loading (Daten werden „bei Bedarf“ nachgeladen) klingt bequem, kann aber innerhalb einer Transaktion unerwartet zusätzliche Queries auslösen. Im ungünstigsten Fall werden dadurch mehr Zeilen gelesen oder gesperrt als geplant. Besser ist, für kritische Pfade explizite Queries zu schreiben, die genau den benötigten Graph laden.
Optimistisches Locking korrekt verdrahten
Optimistische Verfahren funktionieren nur, wenn jedes Update die erwartete Version prüft und die Version anschließend erhöht. Wird das nur bei manchen Updates gemacht, entstehen wieder Lost Updates. Wichtig ist außerdem, Konflikte sauber zu behandeln: Ein Version-Konflikt ist kein 500er, sondern ein erwarteter Fall (z. B. „Datensatz wurde zwischenzeitlich geändert“).
Wer Transaktionen und Isolation als bewusstes Werkzeug einsetzt, bekommt Backends, die unter Parallelität genauso stabil wirken wie im Einzeltest. Der Schlüssel liegt in klaren Invarianten, kleinen Transaktionen, und in der Kombination aus Datenbankregeln, gezielten Sperren und konfliktbewussten Writes.
