Zruck zum Blog, du Zniachtl
Mai 2026·4 Min. zum LesnVo da KI gmocht

Datenbank-Transaktionen und Isolationslevel erklärt

#sql#databases#backend

"Pack es einfach in a Transaktion" is a gängiger Rat, aber a Transaktion alloa garantiert ned, dass zwoa nebenläufige Operationen si ned gegenseitig auf die Zehen steign - des hängt vom Isolationslevel ab, und der Standard in de meisten Datenbanken is schwächer, ois Leit annehmen. Des falsch zu hobn is, wia du an Bug kriagst, der nur unter echter Produktivlast auftaucht, nie in ana Einzelnutzer-Entwicklungsumgebung.

Wos a Transaktion tatsächlich garantiert

A Transaktion gibt dir Atomarität (olle ihre Schreibvorgänge passiern, oda keiner) und Dauerhaftigkeit (einmal committet, überlebt sie an Absturz). De zwoa sand selten in Frage. Wos stark variiert, is die Isolation: wie vül von da laufenden Orbeit oaner Transaktion für a ondere Transaktion sichtbar is, de zur söbn Zeit läuft.

Isolation wird ois Spektrum ausgdrückt. Schwächere Isolation heißt mehr Nebenläufigkeit und besserer Durchsatz, aber mehr Möglichkeiten, wia nebenläufige Transaktionen si gegenseitig stören kinnan. Stärkere Isolation heißt weniger Überraschungen, aber mehr Locking und Contention.

De drei klassischen Anomalien

Bevor die Isolationslevel Sinn ergebn, brauchst die Probleme, noch denen sie benannt sand:

Dirty Read - Transaktion A liest a Zeiln, de Transaktion B gschriebn, aber no ned committet hot. Wenn B dann zruckrollt, hot A Daten glesen, de nie wirklich existiert hom.

Non-repeatable Read - Transaktion A liest a Zeiln zwoamoi und kriagt beide Moi unterschiedliche Werte, weil B dazwischen a Änderung an dera Zeiln committet hot.

Phantom Read - Transaktion A führt dieselbe Bereichs-Query zwoamoi aus (z. B. WHERE amount > 100) und kriagt beim zwoatn Moi a ondere Menge an Zeiln, weil B dazwischen a passende Zeiln eingfügt oda glöscht hot.

De Standard-Isolationslevel

SQL definiert vier Level, jedes verhindert mehr von dem Obigen:

  • Read Uncommitted - verhindert nix. Dirty Reads sand möglich. PostgreSQL implementiert des ned wirklich getrennt (es verhoitet si wia Read Committed); MySQLs InnoDB schon.
  • Read Committed - verhindert Dirty Reads. Du siegst nur committete Daten, aber a Wert kann si zwischn zwoa Lesezugriffen in derselben Transaktion immer no ändern. Des is der Standard in PostgreSQL, Oracle und SQL Server.
  • Repeatable Read - verhindert Dirty Reads und Non-repeatable Reads. Sobois deine Transaktion a Zeiln glesn hot, sieht sie für den Rest der Transaktion denselben Wert, a wenn a ondere Transaktion a Änderung committet. Des is der Standard in MySQLs InnoDB. Phantom Reads sand laut SQL-Standard technisch no möglich, obwohl PostgreSQLs Implementierung (die Snapshot Isolation nutzt) sie tatsächlich a verhindert.
  • Serializable - verhindert olle drei. Transaktionen verhoitn si, ois wärn sie nacheinander in irgendoaner Reihenfolge glaufen, a wenn sie tatsächlich nebenläufig glaufen sand.

Read Committed, wia es dich beißt

Zwoa Transaktionen prüfen beide den Kontostand, bevor sie abbuchen:

-- Transaction A
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- reads 100
-- ...application logic decides it's safe to withdraw 80...
UPDATE accounts SET balance = balance - 80 WHERE id = 1;
COMMIT;
-- Transaction B, running concurrently
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- also reads 100
UPDATE accounts SET balance = balance - 80 WHERE id = 1;
COMMIT;

Wenn beide Transaktionen den Kontostand lesn, bevor eine von beiden committet, glauben beide, es is gnua Göd da. Unter Read Committed blockiert das zwoate UPDATE tatsächlich, bis die erste Transaktion committet (Zeilen-Locking beim Schreibn), und wendet sich dann auf den aktuellen Kontostand von 20 an - allein mit dem rohen SQL kriagst also koan negativen Kontostand. Aber wenn deine Anwendung den Kontostand in oaner Query glesn und "is 80 <= Kontostand" im Anwendungscode validiert hot, bevor sie a separates Update absetzt, dann sitzt genau in dera Prüfen-dann-Handeln-Abfolge die Race Condition, und Read Committed schützt sie in koaner Weise.

Die Lösung: Explizites Locking oda a stärkeres Isolationslevel

Option 1: SELECT ... FOR UPDATE - sperr die glesne Zeiln explizit, sodass a nebenläufige Transaktion, de dieselbe Zeiln liest, blockiert, bis du committest.

BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- application logic
UPDATE accounts SET balance = balance - 80 WHERE id = 1;
COMMIT;

Option 2: Isolationslevel anheben und bei Konflikt wiederholn.

BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT balance FROM accounts WHERE id = 1;
UPDATE accounts SET balance = balance - 80 WHERE id = 1;
COMMIT;

Serializable sperrt ned proaktiv - es erkennt Konflikte beim Commit und bricht oane von den konfliktierenden Transaktionen mit am Serialisierungsfehler ab. Deine Anwendung muass des abfangen und wiederholn:

async function withdrawSerializable(db, accountId, amount) {
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      await db.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
      const { rows } = await db.query('SELECT balance FROM accounts WHERE id = $1', [accountId]);
      if (rows[0].balance < amount) throw new Error('insufficient funds');
      await db.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, accountId]);
      await db.query('COMMIT');
      return;
    } catch (err) {
      await db.query('ROLLBACK');
      if (err.code !== '40001') throw err; // not a serialization failure — don't retry
    }
  }
  throw new Error('could not complete withdrawal after retries');
}

Option 3: A einzelnes atomares Statement, des die gesamte Lesen-dann-Schreiben-Race umgeht:

UPDATE accounts SET balance = balance - 80
WHERE id = 1 AND balance >= 80;
-- check affected row count; 0 means the guard failed

Des is meist die beste Option, wenn's passt - ka explizites Locking, ka Retry-Logik, ka Isolationslevel-Tuning. Es is ned immer anwendbar (manchmoi musst wirklich lesn, nicht-triviale Logik ausführn, dann schreibn), aber wenn a einzelnes UPDATE ... WHERE die ganze Operation ausdrücken kann, is es einfacher und schneller ois jede Alternative.

A Level in da Praxis wählen

Greif ned überoi zu Serializable - es fügt Retry-Komplexität hinzu und senkt den Durchsatz unter Contention. A vernünftiger Standard:

  • Read Committed (oda der Standard deiner Datenbank) für die meisten Operationen.
  • Explizites Zeilen-Locking (FOR UPDATE) für Lesen-dann-Schreiben-Abfolgen auf oaner bestimmten Zeiln, wia im Kontostand-Beispiel.
  • Serializable für Operationen, wo die Korrektheit wirklich davon abhängt, dass die Transaktion über mehrere Statements hinweg an konsistenten Snapshot sieht - Finanz-Hauptbücher, Lagerbestandszählungen, olles, wo a Phantom- oda Non-repeatable Read a falsches Geschäftsergebnis produzieren würd.
  • A atomares Einzel-Statement, wann immer die Operation als eins ausdrückbar is, egal welcher Isolationslevel, weil's das Race-Fenster komplett entfernt.

Der Isolationslevel is a Kompromiss-Regler, ka "Sicherheit an"-Schalter - und der richtige Wert hängt davon ab, vor wöcher konkreten Race du bei ana bestimmten Stelle Code tatsächlich Angst host, ned von ana pauschalen Einstellung für die ganze Anwendung.

NeicherDes Repository-Muster: Vor- und Nachteile, und wann ma's weglassn soitElterRetry-Strategien: Exponential Backoff und Jitter