Read in English · Leer en español · Lire en français
Abschnitt 7.3 von 7.8 · Fortgeschritten · 28 Min. Lesezeit
Transaktionen, ACID und Isolationsebenen
Videolektion
5 Min. · auf Englisch gesprochen, mit UntertitelnNur anhören (Audio)
Transkript lesen
Nebenläufigkeitsfehler bestehen jeden Test auf einem ruhigen Rechner und tauchen dann zur abendlichen Spitzenzeit auf. In dieser Lektion lernen Sie, was Transaktionen wirklich garantieren, wenn viele gleichzeitig laufen, und wie Sie Ihre Regeln schützen.
Stellen Sie sich zwei Kassierer vor, die sich ein Papierkontobuch für das Konto eines Kunden teilen. Jeder schlägt den Kontostand nach, rechnet den neuen auf einem Notizblock aus und trägt ihn ein. Tun sie das im selben Moment, geht die Änderung eines Kassierers verloren. Eine Transaktion bringt Regeln mit, wie sich die Kassierer abwechseln: ob sie in die halb geschriebenen Einträge des anderen schauen dürfen und ob einer warten muss. Strengere Regeln bedeuten weniger Fehler, aber mehr Warten. Und wie Sie sehen werden, bedeuten die Namen dieser Regeln in verschiedenen Datenbanken leicht Unterschiedliches.
Eine Transaktion fasst Lese- und Schreibvorgänge zu einer Einheit zusammen, und das Akronym ACID haben Sie wahrscheinlich schon gehört. Atomarität betrifft Fehler: Geht unterwegs etwas schief, wird jede Änderung rückgängig gemacht, sodass Sie gefahrlos wiederholen können. Konsistenz bedeutet, dass Ihre Regeln gelten, etwa dass Soll gleich Haben ist. Die Datenbank setzt Schlüssel und Constraints durch, aber die meisten Geschäftsregeln halten nur Ihre eigenen Transaktionen ein. Isolation bedeutet, dass sich nebenläufige Transaktionen nicht gegenseitig stören. In ihrer strengsten Form, Serializable, ist das Ergebnis so, als wären sie nacheinander gelaufen. Schwächere Ebenen erlauben für mehr Tempo eine gewisse Beeinflussung. Und Dauerhaftigkeit bedeutet, dass die Daten einen Absturz überstehen, sobald ein Commit bestätigt ist.
Der häufigste Nebenläufigkeitsfehler im Geschäftscode ist das Lost Update. Hier treffen zwei Abhebungen ein Konto mit hundert Pfund. Die erste Transaktion, die dreißig abhebt, liest den Saldo und sieht hundert. Kurz darauf liest die zweite, die fünfzig abhebt, ebenfalls hundert. Die erste schreibt siebzig: hundert minus dreißig. Dann schreibt die zweite fünfzig, hundert minus fünfzig, und überschreibt stillschweigend den ersten Schreibvorgang. Der Saldo müsste zwanzig betragen. Eine Abhebung ist verloren gegangen, obwohl jede Transaktion für sich korrekt war.
Die meisten modernen Datenbanken geben jeder Transaktion mit Multi-Version Concurrency Control einen konsistenten Snapshot. Statt eine Zeile zu überschreiben, erzeugt jeder Schreibvorgang eine neue Version, versehen mit der Nummer der schreibenden Transaktion. Ein Snapshot bei Transaktion hundertfünf sieht die neueste davor festgeschriebene Version: einen Saldo von siebzig. Ein späterer Snapshot, bei hundertzwanzig, sieht zwanzig. Leser warten also nie auf Schreiber, und Schreiber nie auf Leser. Der Preis ist Aufräumarbeit: Eine stundenlang offene Transaktion hält alte Versionen am Leben, und der Speicher bläht sich auf.
Das Lesen aus einem Snapshot heißt Snapshot Isolation. Sie verhindert Lost Updates, lässt aber eine subtilere Anomalie namens Write Skew zu und ist daher nicht vollständig serialisierbar. Namen von Isolationsebenen sind nicht portabel, und die Standardwerte unterscheiden sich. PostgreSQL und Oracle verwenden standardmäßig Read Committed, MySQL Repeatable Read und CockroachDB Serializable. Schlimmer noch: Repeatable Read bedeutet in PostgreSQL und MySQL Verschiedenes. Das von MySQL kann ein Update, das als Lesen und anschließendes Schreiben formuliert ist, trotzdem verlieren. Und Oracles Serializable ist in Wirklichkeit Snapshot Isolation. Code, der auf einer Datenbank korrekt ist, kann auf einer anderen mit gleichnamiger Einstellung falsch sein.
Hier sehen Sie Write Skew in Aktion. Ein Krankenhausdienstplan hat eine Regel: Mindestens ein Arzt muss Bereitschaft haben. Alice und Bob haben Bereitschaft, und beide drücken fast im selben Moment auf „Bereitschaft beenden“. Jede Anfrage zählt die Ärzte in Bereitschaft. Sind es mindestens zwei, setzt sie die eigene Zeile auf „keine Bereitschaft“. Unter Snapshot Isolation zeigen beide Snapshots zwei Ärzte in Bereitschaft, also bestehen beide Prüfungen. Alices Transaktion ändert Alices Zeile, und Bobs ändert Bobs. Verschiedene Zeilen, also wird kein Konflikt erkannt. Beide schreiben fest, und niemand hat Bereitschaft. Unter Serializable bemerkt die Datenbank, dass jede Transaktion sich auf Daten gestützt hat, die die andere danach geändert hat. Ein Commit scheitert, und die Wiederholung zählt erneut, findet nur einen Arzt und lehnt ab. Auf einer schwächeren Ebene können Sie die Zeilen, von denen die Entscheidung abhängt, mit Select for Update sperren. Jetzt wartet Bobs Anfrage, bis Alices fertig ist.
Jeder Code, der einen Wert liest, in der Anwendung entscheidet und zurückschreibt, ist gefährdet. Benennen Sie die Regel, die er schützt, und lassen Sie wo möglich Constraints oder ein einziges bedingtes Update die Arbeit machen. Pessimistisches Sperren sperrt die gelesenen Zeilen bis zu Ihrem Commit. Es passt zu kurzen, stark frequentierten Operationen, etwa einen Kurier einer Bestellung zuzuweisen, um den Preis von Wartezeit und möglichen Deadlocks. Optimistische Kontrolle nimmt keine Sperren. Jede Zeile trägt eine Versionsnummer, und der Schreibvorgang gelingt nur, wenn sich die Version nicht geändert hat. Das passt zu einer Leiterin, die fünf Minuten lang im Browser eine Speisekarte bearbeitet. Oder Sie verwenden Serializable und wiederholen die ganze Transaktion, wenn sie scheitert, also halten Sie E-Mails und Zahlungsaufrufe außerhalb davon. Fachleute sind sich über die beste Voreinstellung uneinig; entscheidend ist eine bewusste, getestete Wahl.
Der vollständige Abschnitt enthält jede Anomalie, einen Vergleich realer Datenbanken und eine Checkliste. Als Nächstes betrachten wir in Abschnitt sieben Punkt vier Replikation und Partitionierung. Bis gleich.
Was Sie wissen müssen
- ACID: Atomarität bedeutet Alles-oder-nichts im Fehlerfall; Konsistenz bedeutet, dass die Invarianten der Anwendung gelten; Isolation begrenzt die Beeinflussung zwischen nebenläufigen Transaktionen; Dauerhaftigkeit bedeutet, dass festgeschriebene Daten Abstürze überstehen, meist durch ein Write-Ahead-Log und Replikate.
- Der SQL-Standard definiert Read Uncommitted, Read Committed, Repeatable Read und Serializable über die Dirty Reads, Non-Repeatable Reads und Phantome, die sie verbieten; Berenson et al. (1995) zeigten, dass diese Definitionen mehrdeutig und unvollständig sind.
- Lost Updates und Write Skew entstehen, wenn Transaktionen Daten lesen, in der Anwendung entscheiden und zurückschreiben; Snapshot Isolation verhindert Lost Updates, lässt aber Write Skew zu.
- MVCC hält mehrere Versionen einer Zeile vor, sodass Leser und Schreiber einander nicht blockieren; lang laufende Transaktionen blähen den Speicher auf.
- Standardwerte und Namen unterscheiden sich: PostgreSQL und Oracle verwenden standardmäßig Read Committed, MySQL InnoDB Repeatable Read, CockroachDB Serializable; Oracles Serializable ist Snapshot Isolation.
- Schützen Sie Read-Modify-Write-Pfade mit Constraints, atomaren bedingten Updates, SELECT ... FOR UPDATE (pessimistisch) oder Versionsspalten (optimistisch), oder verwenden Sie Serializable mit Wiederholung der ganzen Transaktion.
Was eine Transaktion verspricht
Eine Transaktion fasst mehrere Lese- und Schreibvorgänge zu einer Einheit zusammen, die entweder vollständig wirksam wird (Commit) oder gar nicht (Abbruch und Rollback). Kapitel 0 hat die Idee mit einer Banküberweisung eingeführt. In diesem Abschnitt geht es um den Teil, den die meisten Entwicklungsteams missverstehen: was passiert, wenn viele Transaktionen gleichzeitig laufen, welche Garantien Datenbanken wirklich geben und wie man wählt. Die Beispiele verwenden SQL, aber die Überlegungen gelten für jeden Speicher, der Transaktionen anbietet.
Transaktionen sind architektonisch wichtig, weil sie entscheiden, wo sich Geschäftsregeln günstig durchsetzen lassen. Innerhalb einer Datenbank kann eine Transaktion mit wenigen Zeilen Code ein Kontobuch ausgeglichen halten oder verhindern, dass ein Tisch doppelt gebucht wird. Über Services hinweg gibt es kein solches Werkzeug, weshalb Datenhoheit und Sagas so wichtig sind. Wer genau weiß, was eine einzelne Datenbank garantiert, weiß auch, welche Regeln innerhalb der Grenze eines einzigen Eigentümers liegen sollten.
ACID im Detail
ACID wurde 1983 von Theo Härder und Andreas Reuter geprägt, aufbauend auf früheren Arbeiten von Jim Gray. Jeder Buchstabe ist enger gefasst, als er klingt. Atomarität betrifft Fehler, nicht Nebenläufigkeit: Geht unterwegs etwas schief (ein Absturz, eine Constraint-Verletzung, ein Deadlock, die Anwendung entscheidet sich zum Abbruch), wird jede bis dahin vorgenommene Änderung rückgängig gemacht, sodass die Anwendung gefahrlos wiederholen kann. Abbrechbarkeit wäre ein treffenderer Name.
Konsistenz bedeutet, dass eine Transaktion die Datenbank von einem gültigen Zustand in einen anderen überführt, wobei gültig
heißt, dass Ihre Invarianten gelten: Soll gleich Haben, der Bestand ist nie negativ, jede Bestellung hat einen Kunden. Manche Invarianten kann die Datenbank selbst durchsetzen (Schlüssel, Fremdschlüssel, CHECK-Constraints, eindeutige Indizes), die meisten aber sind Geschäftsregeln, die nur die Transaktionen der Anwendung einhalten. Konsistenz ist daher eine Eigenschaft davon, wie die Anwendung Atomarität und Isolation nutzt, nicht etwas, das die Datenbank allein liefert. Sie hat auch nichts mit dem C
in CAP zu tun, das Linearisierbarkeit bedeutet (Kapitel 6).
Isolation bedeutet, dass gleichzeitig laufende Transaktionen einander nicht stören. In ihrer strengsten Form, der Serialisierbarkeit, ist das Ergebnis nebenläufig ausgeführter Transaktionen dasselbe, als wären sie in irgendeiner Reihenfolge nacheinander gelaufen. Schwächere Ebenen erlauben im Tausch gegen Performance eine gewisse Beeinflussung, und der Rest dieses Abschnitts handelt genau davon, welche. Dauerhaftigkeit bedeutet, dass die Daten Abstürze überstehen, sobald ein Commit bestätigt ist. Datenbanken auf einem Knoten erreichen das mit einem Write-Ahead-Log, das auf die Platte geschrieben wird, bevor der Commit zurückkehrt; replizierte Datenbanken fügen Kopien auf anderen Maschinen hinzu. Dauerhaftigkeit ist nie absolut: Eine Platte kann ein Flush nur vortäuschen, und asynchrone Replikation kann die letzten bestätigten Schreibvorgänge verlieren, wenn der Leader ausfällt (siehe Replikation).
Was ohne Isolation schiefgeht
Nebenläufigkeitsfehler sind schwer zu finden, weil sie vom Timing abhängen: Sie bestehen jeden Test auf einem ruhigen Entwicklerrechner und tauchen zur Spitzenzeit am Abend auf. Am klarsten versteht man Isolationsebenen über die konkreten Anomalien (auch Phänomene genannt), die sie zulassen oder verhindern. Das Lost Update unten ist im Geschäftscode die häufigste.
Ein Lost Update, Schritt für Schritt
Die Anomalien, von der mildesten bis zur subtilsten
| Anomalie | Was passiert | Beispiel aus Essenslieferung oder Bank |
|---|---|---|
| Dirty Write | Eine Transaktion überschreibt Daten, die eine andere Transaktion geschrieben, aber noch nicht festgeschrieben hat. | Zwei Transaktionen, die einen Kurier zuweisen, verschränken sich so, dass die Bestellzeile einen Kurier nennt und die Kurierzeile eine andere Bestellung. Jede echte Isolationsebene verhindert das. |
| Dirty Read | Eine Transaktion liest Daten, die eine andere Transaktion geschrieben, aber nicht festgeschrieben hat (und die womöglich zurückgerollt werden). | Ein Bericht zählt eine Zahlung über 500, die anschließend zurückgerollt wird. |
| Non-Repeatable (Fuzzy) Read | Eine Transaktion liest dieselbe Zeile zweimal und erhält unterschiedliche Werte, weil eine andere Transaktion zwischendurch eine Änderung festgeschrieben hat. | Der Checkout liest einen Gerichtpreis von 9,50, eine Änderung des Restaurants schreibt 11,00 fest, und der zweite Lesevorgang des Checkouts sieht 11,00. |
| Read Skew | Eine Transaktion sieht Teile der Datenbank zu unterschiedlichen Zeitpunkten, sodass zusammengehörige Werte nicht aufgehen. | Ein Kunde mit 500 auf zwei Konten sieht insgesamt 450, weil zwischen dem Lesen der beiden Salden eine Überweisung von 50 festgeschrieben wurde. |
| Phantom Read | Eine Transaktion führt eine Abfrage mit einer Bedingung erneut aus und findet Zeilen, die eine andere, festgeschriebene Transaktion hinzugefügt oder entfernt hat. | Zweimaliges Zählen der offenen Lieferungen eines Kuriers liefert erst 2, dann 3. |
| Lost Update | Zwei Transaktionen lesen einen Wert, jede berechnet einen neuen und schreibt ihn, und ein Schreibvorgang überschreibt stillschweigend den anderen. | Zwei Abhebungen vom selben Konto (Diagramm oben). |
| Write Skew | Zwei Transaktionen lesen überlappende Daten, treffen darauf gestützt Entscheidungen und ändern verschiedene Zeilen, sodass sie gemeinsam eine Regel verletzen, die jede für sich geprüft hat. | Zwei Ärzte in Bereitschaft sehen jeweils, dass noch jemand anderes Bereitschaft hat, und beenden beide ihre Bereitschaft (ausgearbeitetes Beispiel unten). |
Die Isolationsebenen des SQL-Standards
Der SQL-Standard (ab SQL-92) definiert vier Isolationsebenen danach, welche von drei Phänomenen sie verbieten: Dirty Reads, Non-Repeatable Reads und Phantome. Der Standard macht Serializable zur Voreinstellung, doch kaum ein gängiges Produkt hält sich daran; die meisten verwenden aus Performancegründen standardmäßig Read Committed oder Repeatable Read.
Die Ebenen des SQL-Standards und die Phänomene, die sie ausschließen
| Isolationsebene | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| Read Uncommitted | möglich | möglich | möglich |
| Read Committed | verhindert | möglich | möglich |
| Repeatable Read | verhindert | verhindert | möglich |
| Serializable | verhindert | verhindert | verhindert, und das Ergebnis muss einer seriellen Reihenfolge entsprechen |
1995 veröffentlichten Hal Berenson, Phil Bernstein, Jim Gray, Jim Melton, Elizabeth O'Neil und Patrick O'Neil A Critique of ANSI SQL Isolation Levels. Sie zeigten, dass die englischsprachigen Definitionen des Standards mehrdeutig waren, dass das Verbot der drei Phänomene allein noch keine Serialisierbarkeit ergibt und dass die Ebenen des Standards die Isolationsebenen realer Produkte nicht beschrieben. Sie ergänzten Phänomene, die der Standard übersehen hatte, namentlich Dirty Write, Lost Update, Read Skew und Write Skew, und definierten Snapshot Isolation: Jede Transaktion liest aus einem konsistenten Snapshot der festgeschriebenen Daten zum Zeitpunkt ihres Starts, und eine Transaktion kann keinen Schreibvorgang auf eine Zeile festschreiben, die eine nebenläufige Transaktion bereits geändert und festgeschrieben hat (First Committer Wins
). Snapshot Isolation verhindert Dirty Reads, Non-Repeatable Reads, Read Skew und Lost Updates sowie die Phantome, die Lesevorgänge betreffen, lässt aber Write Skew zu und ist daher nicht serialisierbar. Sie passt nur schlecht zwischen die Ebenen des Standards, weshalb Produkte sie unterschiedlich benennen.
MVCC: wie Snapshots funktionieren
Die meisten modernen Datenbanken setzen Snapshot-Lesevorgänge mit Multi-Version Concurrency Control (MVCC) um. Statt eine Zeile an Ort und Stelle zu überschreiben, erzeugt ein Schreibvorgang eine neue Version, die mit der Kennung der schreibenden Transaktion versehen ist, und die alte Version bleibt erhalten, bis keine laufende Transaktion sie mehr brauchen kann. Jeder Leser erhält einen Snapshot, im Wesentlichen eine Liste der Transaktionen, die bei seinem Start festgeschrieben waren, und sieht nur die Versionen, die diese Transaktionen erzeugt haben. Der große Vorteil: Leser warten nie auf Schreiber und Schreiber nie auf Leser; nur zwei Schreiber auf dieselbe Zeile geraten in Konflikt.
Zeilenversionen unter MVCC
Der Preis ist Aufräumarbeit. Alte Versionen müssen entfernt werden: PostgreSQL hält sie in der Tabelle und entfernt sie mit Vacuum, Oracle und MySQL InnoDB halten Before-Images in Undo-Logs. Eine stundenlang offene Transaktion hält alte Versionen fest, sodass Tabellen und Undo-Logs anwachsen und die Performance sinkt. Lang laufende Transaktionen sind daher in jeder MVCC-Datenbank ein betriebliches Warnsignal und ein Grund, analytische Abfragen von der operativen Datenbank fernzuhalten. Unter Read Committed mit MVCC erhält jede Anweisung einen frischen Snapshot; bei Snapshot Isolation verwendet die ganze Transaktion einen einzigen.
Echte Standardwerte und warum die Namen in die Irre führen
Namen von Isolationsebenen sind nicht portabel. Dasselbe Wort kann in verschiedenen Produkten unterschiedliche Garantien bedeuten, und die Standardwerte unterscheiden sich. Martin Kleppmanns Projekt Hermitage hat das tatsächliche Verhalten vieler Datenbanken gegen die obigen Anomalien getestet; die Herstellerdokumentation bestätigt die Standardwerte. Betrachten Sie diese Tabelle als Ausgangspunkt und prüfen Sie die Dokumentation der Version, die Sie betreiben.
Gleiche Namen, anderes Verhalten: was reale Datenbanken tun (zusammengefasst aus Herstellerdokumentation und den Hermitage-Tests)
| Datenbank | Standardebene | Was die Ebenen wirklich sind | Anmerkungen |
|---|---|---|---|
| PostgreSQL | Read Committed | Read Uncommitted verhält sich wie Read Committed. Repeatable Read ist Snapshot Isolation. Serializable ist Serializable Snapshot Isolation (SSI). | Repeatable Read bricht eine Transaktion ab, die eine seit ihrem Snapshot geänderte Zeile ändern will (could not serialize access due to concurrent update), sodass Lost Updates verhindert werden; Write Skew nicht. |
| MySQL (InnoDB) | Repeatable Read | Einfache SELECTs lesen einen Snapshot, der beim ersten Lesen entsteht; sperrende Lesevorgänge, UPDATE und DELETE arbeiten mit Record- und Gap-Locks auf den neuesten festgeschriebenen Zeilen. Serializable macht einfache SELECTs zu sperrenden Lesevorgängen, wenn Autocommit aus ist. | Bei Repeatable Read kann ein Read-Modify-Write, geschrieben als SELECT und dann UPDATE, immer noch ein Update verlieren oder Write Skew erzeugen, sofern das SELECT nicht FOR UPDATE verwendet. |
| Oracle Database | Read Committed | Bietet Read Committed und Serializable; sein Serializable ist Snapshot Isolation. | Verhindert Lost Updates bei Serializable, lässt aber Write Skew zu. |
| Microsoft SQL Server | Read Committed (sperrend) auf SQL Server; Azure SQL Database schaltet standardmäßig Read Committed Snapshot ein | Sperrendes Read Committed, Read Committed Snapshot, Repeatable Read (sperrend), Snapshot (Snapshot Isolation) und Serializable (sperrend). | Auf SQL Server muss Snapshot Isolation vor der Nutzung pro Datenbank aktiviert werden (ALLOW_SNAPSHOT_ISOLATION). |
| CockroachDB | Serializable | Standardmäßig Serializable; Read Committed ist ebenfalls verfügbar. | Clients müssen Transaktionen wiederholen, die mit einem Serialisierungsfehler scheitern. |
Serialisierbarkeit erreichen
Datenbanken mit echter Serialisierbarkeit verwenden eine von drei Techniken. Tatsächlich serielle Ausführung lässt Transaktionen nacheinander auf einem einzigen Thread laufen, was schnell ist, wenn jede Transaktion kurz ist und die Daten im Arbeitsspeicher liegen (VoltDB sowie Redis-Skripte und -Transaktionen arbeiten so). Two-Phase Locking (2PL), verwendet von den Serializable-Ebenen von SQL Server und MySQL, lässt Leser gemeinsame Sperren und Schreiber exklusive Sperren nehmen, einschließlich Bereichssperren gegen Phantome, und hält sie bis zum Ende der Transaktion; das ist korrekt, aber Leser und Schreiber blockieren einander, der Durchsatz sinkt bei Konkurrenz, und Deadlocks sind häufig. Serializable Snapshot Isolation (SSI), 2008 von Michael Cahill, Uwe Röhm und Alan Fekete beschrieben und in PostgreSQL seit Version 9.1 verwendet (CockroachDB nutzt eine verwandte optimistische Technik), führt Transaktionen ohne Blockieren auf Snapshots aus, verfolgt Lese-Schreib-Abhängigkeiten zwischen ihnen und bricht eine Transaktion ab, wenn ein gefährliches Muster auftritt.
Welche Technik auch verwendet wird: Serialisierbare Transaktionen können mit einem Serialisierungsfehler (SQLSTATE 40001 in PostgreSQL und CockroachDB) oder einem Deadlock scheitern, und die Anwendung muss die ganze Transaktion wiederholen. Das sind die versteckten Kosten der stärksten Ebene: Jeder transaktionale Codepfad braucht eine Wiederholungsschleife, die gefahrlos wiederholbar ist, also keine Nebenwirkungen wie den Versand von E-Mails oder Aufrufe von Zahlungsanbietern innerhalb der Transaktion.
Pessimistische und optimistische Nebenläufigkeitskontrolle
In der Praxis laufen die meisten Anwendungen mit Read Committed oder Repeatable Read und schützen bestimmte Read-Modify-Write-Pfade explizit. Dafür gibt es drei Werkzeuge. Das beste, wenn möglich, ist ein atomares Update, bei dem die Datenbank Rechnung und Prüfung in einer Anweisung erledigt, etwa UPDATE accounts SET balance = balance - 30 WHERE id = 7 AND balance >= 30, und bei dem man danach prüft, dass genau eine Zeile geändert wurde. Unter Read Committed in PostgreSQL lässt ein nebenläufiges Update die zweite Anweisung warten und ihre Bedingung dann gegen die neue Zeile neu auswerten, sodass kein Update verloren geht.
Braucht die Entscheidung Logik, die die Datenbank nicht ausdrücken kann, verwenden Sie pessimistisches Sperren: Sperren Sie die gelesenen Zeilen mit SELECT ... FOR UPDATE, sodass nebenläufige Transaktionen, die sie brauchen, bis zu Ihrem Commit warten. Das ist einfach und passt zu kurzen, stark umkämpften Operationen (einen Kurier einer Bestellung zuweisen). Die Kosten sind Wartezeit, das Risiko von Deadlocks (sperren Sie Zeilen in einer festen Reihenfolge, etwa nach aufsteigender id) und geringerer Durchsatz. Varianten sind NOWAIT, das sofort scheitert, statt zu warten, und SKIP LOCKED, das bereits gesperrte Zeilen überspringt und der übliche Weg ist, eine zuverlässige Job-Queue auf einer relationalen Tabelle zu bauen.
BEGIN;
-- Lock the row: other FOR UPDATE readers and writers must wait
SELECT balance FROM accounts WHERE id = 7 FOR UPDATE;
-- application checks: balance (100) >= 30, so allow it
UPDATE accounts SET balance = 70 WHERE id = 7;
INSERT INTO ledger (account_id, amount, kind)
VALUES (7, -30, 'withdrawal');
COMMIT; -- lock released; the waiting transaction now sees 70Optimistische Nebenläufigkeitskontrolle nimmt keine Sperren, während der Nutzer oder der Code nachdenkt. Jede Zeile trägt eine Versionsnummer (oder einen Zeitstempel oder Hash). Der Schreiber ändert nur, wenn die Version noch die gelesene ist, und erhöht sie. Ändert sich keine Zeile, war jemand anderes schneller, und die Anwendung lädt neu und wiederholt oder informiert den Nutzer. Das passt zu Daten mit wenig Konkurrenz und langer Denkzeit
: Eine Restaurantleiterin, die fünf Minuten lang im Browser eine Speisekarte bearbeitet, darf nicht so lange eine Datenbanksperre halten. HTTP hat dieselbe Idee eingebaut, mit ETags und dem Header If-Match.
-- Read (no lock); remember version = 12
SELECT id, name, price, version FROM menu_items WHERE id = 501;
-- Later, write only if the version is unchanged
UPDATE menu_items
SET price = 11.00, version = version + 1
WHERE id = 501 AND version = 12;
-- Rows affected = 1: success.
-- Rows affected = 0: a concurrent edit won; reload and retry
-- or show the user the other change.Ausgearbeitetes Beispiel
Ausgearbeitetes Beispiel: Write Skew mit zwei Ärzten in Bereitschaft
Ein Dienstplansystem in einem Krankenhaus setzt eine Regel durch: Für jede Schicht muss mindestens ein Arzt Bereitschaft haben. Alice und Bob haben heute Nacht beide Bereitschaft. Beide fühlen sich unwohl und drücken fast im selben Moment auf Bereitschaft beenden
. Jede Anfrage führt diese Logik in einer Transaktion aus: Zähle die Ärzte in Bereitschaft für die Schicht; sind es mindestens zwei, setze meine eigene Zeile auf „keine Bereitschaft“. Die Datenbank läuft mit Snapshot Isolation (Repeatable Read in PostgreSQL oder Serializable in Oracle).
| Start | Tabelle doctors: Alice on_call = true, Bob on_call = true. Regel: count(on_call) >= 1. |
|---|---|
| Schritt 1: T1 (Alice) beginnt | T1 nimmt einen Snapshot. |
| Schritt 2: T2 (Bob) beginnt | T2 nimmt einen Snapshot. Beide Snapshots zeigen zwei Ärzte in Bereitschaft. |
| Schritt 3: T1 liest | SELECT count(*) FROM doctors WHERE shift = 1234 AND on_call liefert 2. Prüfung bestanden. |
| Schritt 4: T2 liest | Dieselbe Abfrage liefert auch im Snapshot von T2 den Wert 2. Prüfung bestanden. |
| Schritt 5: T1 schreibt | UPDATE doctors SET on_call = false WHERE name = 'Alice' AND shift = 1234 |
| Schritt 6: T2 schreibt | UPDATE doctors SET on_call = false WHERE name = 'Bob' AND shift = 1234. Eine andere Zeile, daher wird kein Schreib-Schreib-Konflikt erkannt. |
| Schritt 7: beide schreiben fest | Snapshot Isolation sieht keinen Konflikt: Jede Transaktion hat eine Zeile geschrieben, die die andere nicht geschrieben hat. Ergebnis: Niemand hat Bereitschaft. Jede Transaktion war für sich korrekt; gemeinsam haben sie die Regel verletzt. |
| Unter Serializable (SSI) | Die Datenbank bemerkt, dass jede Transaktion Daten gelesen hat, die die andere danach geändert hat (ein gefährlicher Zyklus von Lese-Schreib-Abhängigkeiten). Ein Commit scheitert mit SQLSTATE 40001; die Wiederholung zählt neu, findet einen Arzt und lehnt ab. |
| Lösung auf einer schwächeren Ebene | Sperren Sie die Zeilen, von denen die Entscheidung abhängt: SELECT name FROM doctors WHERE shift = 1234 AND on_call FOR UPDATE, und zählen Sie dann die zurückgegebenen Zeilen. Jetzt wartet T2 auf die Sperren von T1. Unter Read Committed liest T2 danach erneut, findet nur Bob in Bereitschaft und lehnt ab; unter Repeatable Read in PostgreSQL scheitert es stattdessen mit einem Serialisierungsfehler, und die Wiederholung lehnt ab. |
Write Skew ist eine Verallgemeinerung des Lost Update: Die Transaktionen lesen dieselben Daten, schreiben aber verschiedene Zeilen, sodass die First-Committer-Wins-Regel der Snapshot Isolation nie greift.
Dasselbe Muster tritt in Buchungssystemen auf (zwei Personen buchen den letzten Platz oder überlappende Besprechungstermine), bei Benutzernamen (zwei Registrierungen prüfen, ob ein Name frei ist) und im Bankwesen (zwei Abhebungen von verschiedenen Konten, die zusammen ein gemeinsames Überziehungslimit überschreiten).
Wenn die Zeilen, die einen Konflikt belegen würden, noch nicht existieren, wie bei zwei überlappenden Buchungen, hat FOR UPDATE nichts zu sperren. Möglich sind dann Serializable, ein Unique- oder Exclusion-Constraint (PostgreSQL kann überlappende Zeiträume deklarativ verbieten) oder das Materialisieren des Konflikts: Man legt Zeilen an, etwa eine pro Raum und Zeitfenster, nur damit sie sich sperren lassen.
Eine Isolationsstrategie wählen
Es gibt nicht die eine richtige Ebene; es gibt eine richtige Ebene für jedes Stück Logik. Ein praktischer Ansatz geht von den Invarianten aus, nicht von den Einstellungen.
Checkliste: einen transaktionalen Codepfad sicher machen
- Die Invariante benennenSchreiben Sie die Regel auf, die der Code schützt (Saldo nie unter null, ein Kurier pro Bestellung, mindestens ein Arzt in Bereitschaft). Gibt es keine, genügt wahrscheinlich die Standardebene.
- Constraints und atomare Anweisungen bevorzugenEindeutige Indizes, Fremdschlüssel, CHECK- und Exclusion-Constraints sowie bedingte Updates in einer einzigen Anweisung werden von der Datenbank unabhängig von der Isolationsebene durchgesetzt.
- Read-Modify-Write erkennenJeder Code, der einen Wert liest, in der Anwendung entscheidet und zurückschreibt, birgt das Risiko eines Lost Update oder Write Skew. Schützen Sie ihn mit FOR UPDATE, einer Versionsspalte oder Serializable.
- Die echte Semantik Ihrer Datenbank kennenPrüfen Sie, was die Ebenennamen Ihres Produkts tatsächlich bedeuten (Repeatable Read in PostgreSQL und MySQL unterscheiden sich; Serializable in Oracle ist Snapshot Isolation).
- Richtig wiederholenSerializable und Repeatable Read können mit Serialisierungsfehlern und Deadlocks abbrechen. Wiederholen Sie die ganze Transaktion mit Backoff, und halten Sie externe Nebenwirkungen außerhalb davon.
- Transaktionen kurz haltenKeine Denkzeit von Nutzern, keine Netzwerkaufrufe und keine großen Berichte innerhalb einer Transaktion; lange Transaktionen halten Sperren und blähen den MVCC-Speicher auf.
- Unter Nebenläufigkeit testenFühren Sie die kritischen Pfade in einem Integrationstest nebenläufig gegen die echte Datenbank-Engine aus, nicht gegen einen In-Memory-Ersatz mit anderer Semantik.
Als Faustregel ist Read Committed mit expliziten Sperren oder Versionsprüfungen für die meisten Geschäftsanwendungen eine solide, schnelle Voreinstellung, sofern das Team diszipliniert nach Read-Modify-Write-Pfaden sucht. Serializable ist die sicherere Voreinstellung, wenn die Regeln komplex sind, das Team groß ist oder eine verletzte Invariante teuer wird (Geld, Medizin, Betriebssicherheit), und viele Teams setzen es inzwischen für die kritischen Pfade ein und nehmen Wiederholungen in Kauf. Hier sind sich Fachleute wirklich uneinig: Manche argumentieren, dass Serializable als Voreinstellung eine ganze Klasse von Fehlern verhindert, die kein Code-Review je findet; andere argumentieren, dass seine Wiederholungen und seine Konkurrenz um Ressourcen zu viel kosten und dass explizite Sperren die Absicht sichtbar machen. Beide Positionen sind vertretbar, wenn die Wahl bewusst getroffen und getestet ist.
Prüfen Sie Ihr Verständnis
Warum ist Snapshot Isolation nicht serialisierbar?
Sie lässt Write Skew zu: Zwei Transaktionen können überlappende Daten lesen und verschiedene Zeilen schreiben und so gemeinsam eine Regel verletzen, die jede geprüft hat, ohne dass ein Schreib-Schreib-Konflikt erkannt wird.
Wovon hängt das C in ACID ab?
Hauptsächlich von der Anwendung: Die Datenbank setzt deklarierte Constraints durch, aber Geschäftsinvarianten gelten nur, wenn die Transaktionen der Anwendung sie wahren.
Wann würden Sie optimistische statt pessimistischer Nebenläufigkeitskontrolle wählen?
Wenn Konflikte selten sind oder zwischen Lesen und Schreiben eine lange Denkzeit liegt, etwa wenn ein Nutzer ein Formular bearbeitet, sodass das Halten von Sperren verschwenderisch oder unmöglich wäre.
Warum muss Code, der Serializable verwendet, gefahrlos wiederholbar sein?
Serialisierbare Transaktionen können durch Serialisierungsfehler oder Deadlocks abgebrochen werden, und die richtige Reaktion ist, die ganze Transaktion erneut auszuführen; daher darf sie keine externen Nebenwirkungen haben.
Quellen und weiterführende Literatur
Haben Sie eine Frage zu diesem Abschnitt? ArchitectBot, der KI-Lernassistent des Kurses, antwortet zuerst aus dem Kurs.
Fragen: „Unser Buchungssystem bucht gelegentlich einen Tisch doppelt, obwohl wir vorher die Verfügbarkeit prüfen. Welche Isolationsebene oder welchen Sperransatz sollten wir verwenden?“