Verständnis von serielisierter Isolation und ihren Leistungsauswirkungen
James Reed
Infrastructure Engineer · Leapcell

Einführung
In der komplexen Welt der Datenbankverwaltung ist die Gewährleistung der Datenkonsistenz inmitten gleichzeitiger Operationen eine vordringliche Herausforderung. Wenn Anwendungen in Umfang und Benutzeraktivität zunehmen, versuchen oft mehrere Transaktionen gleichzeitig, auf dieselben Daten zuzugreifen und diese zu ändern. Ohne ordnungsgemäße Mechanismen zur Steuerung dieser Interaktionen kann die Integrität unserer Daten erheblich beeinträchtigt werden, was zu falschen Ergebnissen, verlorenen Updates oder sogar beschädigten Daten führt. Hier kommen die Isolationsstufen von Datenbanken ins Spiel, die als wichtige Hüter der Datenkonsistenz fungieren. Unter ihnen sticht die Serializable
-Isolationsstufe als die strengste hervor und bietet das höchste Maß an Datenintegrität. Diese Robustheit geht jedoch mit einem erheblichen Kompromiss einher, der oft die Leistung beeinträchtigt. Dieser Artikel zielt darauf ab, die Serializable
-Isolationsstufe eingehend zu untersuchen, ihre zugrunde liegenden Prinzipien und praktischen Implementierungen zu entschlüsseln und ihre Leistungsauswirkungen kritisch zu prüfen, um eine Roadmap für ihre umsichtige Anwendung zu bieten.
Verständnis der seriellen Isolation
Bevor wir uns mit den Feinheiten von Serializable
befassen, lassen Sie uns einige grundlegende Konzepte klären, die die gleichzeitige Steuerung von Datenbanken untermauern.
Kernterminologie
- Transaktion: Eine einzelne, logische Arbeitseinheit, die auf eine Datenbank zugreift und diese möglicherweise modifiziert. Transaktionen sollen die Datenintegrität und -konsistenz wahren. Sie zeichnen sich durch ACID-Eigenschaften aus: Atomarität, Konsistenz, Isolation und Dauerhaftigkeit.
- Concurrency Control (Gleichzeitige Steuerung): Das Prinzip der Verwaltung gleichzeitiger Datenbankoperationen, so dass die Datenintegrität erhalten bleibt und Konflikte auf vorhersagbare Weise gelöst werden.
- Isolation Levels (Isolationsebenen): Ein Satz von Standards, die von SQL definiert und oft von spezifischen Datenbanksystemen erweitert werden und festlegen, wie die Datenänderungen einer Transaktion für andere gleichzeitige Transaktionen sichtbar sind. Höhere Isolationsebenen bieten größeren Schutz vor Nebenläufigkeitsanomalien, verursachen aber typischerweise höhere Leistungskosten.
- Concurrency Anomalies (Nebenläufigkeitsanomalien): Verschiedene Arten von Fehlern oder unerwarteten Verhaltensweisen, die auftreten können, wenn mehrere Transaktionen gleichzeitig ohne ordnungsgemäße Isolation ausgeführt werden. Häufige Anomalien sind:
- Dirty Reads (Unbestätigtes Lesen): Eine Transaktion liest Daten, die von einer anderen unbestätigten Transaktion geschrieben wurden.
- Non-Repeatable Reads (Nicht wiederholbare Lesevorgänge): Eine Transaktion liest Daten erneut, die zuvor gelesen wurden, und stellt fest, dass diese von einer anderen bestätigten Transaktion geändert wurden.
- Phantom Reads (Phantomlesevorgänge): Eine Transaktion führt eine Abfrage erneut aus, die eine Menge von Zeilen zurückgibt, und stellt fest, dass sich die Menge der Zeilen, die die Abfrage erfüllen, aufgrund einer anderen bestätigten Transaktion geändert hat (z. B. neue Zeilen hinzugefügt oder vorhandene Zeilen gelöscht wurden).
- Lost Updates (Verlorene Updates): Zwei Transaktionen lesen dieselben Daten, modifizieren sie und schreiben sie zurück. Eines der Updates geht "verloren", da die andere Transaktion es überschreibt, ohne das erste Update zu berücksichtigen.
- Serial Execution (Serielle Ausführung): Die Ausführung von Transaktionen nacheinander, ohne jegliche Überlappung. Dies garantiert inhärent die Konsistenz, ist jedoch für gleichzeitige Systeme aufgrund des extrem schlechten Dursatzes unpraktisch.
- Serialization (Serialisierung): Der Prozess der Sicherstellung, dass gleichzeitige Transaktionen die gleichen Ergebnisse liefern, als ob sie nacheinander (seriell) in einer bestimmten Reihenfolge ausgeführt worden wären.
Was ist serielle Isolation?
Die Serializable
-Isolationsstufe ist die stärkste Isolationsstufe, die vom SQL-Standard definiert wird. Sie garantiert, dass alle gleichzeitig ausgeführten Transaktionen, wenn sie bestätigt werden, das gleiche Ergebnis liefern, als ob sie sequenziell (seriell) in einer bestimmten Reihenfolge ausgeführt worden wären. Im Wesentlichen eliminiert sie alle gängigen Nebenläufigkeitsanomalien, einschließlich Dirty Reads, Non-Repeatable Reads und Phantom Reads. Sie stellt sicher, dass sich Transaktionen so verhalten, als ob sie während ihrer gesamten Dauer exklusiven Zugriff auf die Datenbank hätten.
Wie serielle Isolation funktioniert
Datenbanksysteme erreichen die Serializable
-Isolation typischerweise durch entweder Two-Phase Locking (2PL) oder optimistische Concurrency Control (OCC)-Varianten, oft kombiniert mit Multi-Version Concurrency Control (MVCC)-Techniken in moderneren Systemen.
Two-Phase Locking (2PL)
Bei reinem 2PL erwirbt eine Transaktion Sperren (gemeinsam für Lesevorgänge, exklusiv für Schreibvorgänge) für Datenelemente. Sie hat eine Wachstumsphase, in der sie neue Sperren erwerben kann, und eine Schrumpfungsphase, in der sie Sperren freigeben kann. Die kritische Regel für 2PL ist, dass eine Transaktion nach der Freigabe einer Sperre keine neuen Sperren mehr erwerben kann. Für Serializable
-Isolation muss 2PL strict 2PL sein, was bedeutet, dass alle Sperren gehalten werden, bis die Transaktion bestätigt oder abgebrochen wird. Um Phantomlesevorgänge zu verhindern, verwendet Serializable
oft Prädikatssperren oder Bereichssperren, die nicht nur einzelne Zeilen sperren, sondern auch die Möglichkeit, dass Zeilen innerhalb eines bestimmten Bereichs hinzugefügt oder entfernt werden oder einem bestimmten Prädikat entsprechen.
Betrachten wir ein Beispiel für eine Banküberweisung:
-- Transaktion A: Überweisung von 100 $ von Konto 1 auf Konto 2 BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; SELECT balance FROM Accounts WHERE account_id = 1 FOR UPDATE; -- Sperre Konto 1 UPDATE Accounts SET balance = balance - 100 WHERE account_id = 1; SELECT balance FROM Accounts WHERE account_id = 2 FOR UPDATE; -- Sperre Konto 2 UPDATE Accounts SET balance = balance + 100 WHERE account_id = 2; COMMIT; -- Transaktion B: Prüfen des Gesamtbetrags der Konten 1, 2 und 3 BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; SELECT SUM(balance) FROM Accounts WHERE account_id IN (1, 2, 3); COMMIT;
Mit Serializable
-Isolation unter Verwendung von Strict 2PL, wenn Transaktion A zuerst startet, erwirbt sie exklusive Sperren für Konto 1 und Konto 2. Transaktion B, die versucht, diese Konten zu lesen, würde blockiert, bis Transaktion A bestätigt wird. Wenn Transaktion A bestätigt wird, kann Transaktion B dann die aktualisierten Salden lesen. Dies verhindert, dass Transaktion B Zwischenzustände (Dirty Reads) sieht oder unterschiedliche Summen, wenn sie später Daten erneut liest (Non-Repeatable Reads). Wenn Transaktion B versucht, eine Summe von Salden zu lesen, erwirbt sie möglicherweise gemeinsame Sperren für alle relevanten Konten, und wenn Transaktion A eines dieser Konten ändern möchte, wird sie blockiert, bis Transaktion B fertig ist, was potenziell zu Deadlocks führt.
Snapshot Isolation (oft Grundlage für "Serializable" in MVCC-Datenbanken)
Viele moderne relationale Datenbanken wie PostgreSQL und Oracle implementieren ihre Serializable
-Isolationsstufe (oder eine gleichwertige wie Oracles SERIALIZABLE
) durch eine Kombination aus Multi-Version Concurrency Control (MVCC) und einer zusätzlichen Validierungsschicht über Snapshot Isolation.
- MVCC: Anstatt Daten direkt zu überschreiben, erstellen MVCC-Datenbanken eine neue Version einer Zeile, wenn sie aktualisiert wird. Leser sehen typischerweise einen konsistenten "Schnappschuss" der Datenbank aus der Zeit, als ihre Transaktion begann, ohne Schreiber zu blockieren. Dies verhindert inhärent Dirty Reads und Non-Repeatable Reads.
- Snapshot Isolation: Eine Transaktion, die unter Snapshot-Isolation arbeitet, sieht einen Schnappschuss der Datenbank, wie er zu Beginn der Transaktion existierte. Schreibvorgänge anderer Transaktionen, die nach der Erstellung des Schnappschusses bestätigt wurden, sind nicht sichtbar. Dies verhindert Dirty Reads und Non-Repeatable Reads. Allerdings verhindert Snapshot Isolation allein nicht Phantom-Lesevorgänge oder eine bestimmte Anomalie namens "Write Skew"-Anomalie.
Um vollständige Serializable
-Isolation über MVCC/Snapshot-Isolation hinaus zu erreichen, fügen Datenbanken eine zusätzliche Validierungsebene zum Zeitpunkt des Commits hinzu. Diese Validierung prüft, ob die Modifikationen (Schreibvorgänge) der bestätigten Transaktion mit anderen gleichzeitig von anderen Transaktionen getätigten Schreibvorgängen, die nach der Erstellung des Schnappschusses vorgenommen wurden, auf eine Weise kollidieren, die die Serialisierbarkeit verletzen würde. Wenn ein solcher Konflikt erkannt wird, wird ein Update abgelehnt und die Transaktion wird typischerweise abgebrochen, was die Anwendung zwingt, sie erneut zu versuchen. Dies wird oft als Serializable Snapshot Isolation (SSI) bezeichnet.
Lassen Sie uns das Banküberweisungsszenario mit SSI noch einmal betrachten:
-- Transaktion A: Überweisung von 100 $ von Konto 1 auf Konto 2 BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- Liest aktuelle Salden aus seinem Schnappschuss SELECT balance FROM Accounts WHERE account_id = 1; SELECT balance FROM Accounts WHERE account_id = 2; -- Führt Berechnungen durch UPDATE Accounts SET balance = balance - 100 WHERE account_id = 1; UPDATE Accounts SET balance = balance + 100 WHERE account_id = 2; COMMIT; -- Beim Commit erfolgt die Konflikterkennung. -- Transaktion C: Überweisung von 50 $ von Konto 2 auf Konto 3 BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- Liest aktuelle Salden aus seinem Schnappschuss SELECT balance FROM Accounts WHERE account_id = 2; SELECT balance FROM Accounts WHERE account_id = 3; -- Führt Berechnungen durch UPDATE Accounts SET balance = balance - 50 WHERE account_id = 2; UPDATE Accounts SET balance = balance + 50 WHERE account_id = 3; COMMIT; -- Beim Commit erfolgt die Konflikterkennung.
Wenn Transaktion A und Transaktion C beide ungefähr zur gleichen Zeit starten und versuchen, Konto 2 zu ändern, wird eine von ihnen zum Zeitpunkt des Commits aufgrund eines Serialisierungskonflikts fehlschlagen. Wenn beispielsweise Transaktion A zuerst bestätigt wird, würde Transaktion C beim Versuch, zu bestätigen, feststellen, dass ihre Lesung von account_id = 2
auf einem Schnappschuss basierte, der jetzt auf eine Weise "veraltet" ist, die die Serialisierbarkeit verletzt (d. h. ihre Modifikationen kollidieren mit As Modifikationen auf demselben Konto). Transaktion C würde abgebrochen und die Anwendung müsste sie erneut versuchen.
Leistungsauswirkungen
Obwohl die Serializable
-Isolation der Goldstandard für Datenkonsistenz ist, führt ihre Implementierung unweigerlich zu erheblichen Leistungseinbußen.
-
Erhöhter Sperr-Overhead (für 2PL-basierte Systeme):
- Längere Haltedauer von Sperren: Sperren werden bis zum Ende der Transaktion (Commit oder Rollback) gehalten. Dies reduziert die Nebenläufigkeit, da andere Transaktionen länger blockiert werden.
- Höhere Sperrkonflikte: Mehr Transaktionen konkurrieren um dieselben Sperren, was zu längeren Wartezeiten führt.
- Deadlocks: Wenn zwei oder mehr Transaktionen gegenseitig auf Sperren warten, die von anderen gehalten werden, tritt ein Deadlock auf. Datenbanksysteme erkennen und lösen Deadlocks, indem sie eine der Transaktionen abbrechen, was verschwendete Arbeit und Wiederholungsversuche bedeutet.
- Reduzierter Durchsatz: Der kombinierte Effekt von längeren Sperrhaltedauern und Konflikten begrenzt letztendlich die Anzahl der Transaktionen pro Sekunde, die verarbeitet werden können.
-
Erhöhter Overhead für optimistische Concurrency Control (für MVCC-basierte Systeme mit SSI):
- Transaktionsabbrüche/Wiederholungsversuche: Während MVCC die Blockierung reduziert, bricht die Commit-Zeit-Validierung für
Serializable
-Isolation proaktiv Transaktionen ab, die die Serialisierbarkeit verletzen. Die Anwendung muss so konzipiert sein, dass abgebrochene Transaktionen wieder ausgeführt werden, was CPU-Zyklen und potenziell Netzwerkressourcen wiederholt verbraucht. - Erhöhte Latenz: Eine Transaktion, die wiederholt werden muss, benötigt länger, um erfolgreich bestätigt zu werden, was die Benutzererfahrung beeinträchtigt.
- CPU-Overhead für Konflikterkennung: Das Datenbanksystem muss zum Zeitpunkt des Commits zusätzliche Prüfungen durchführen, um Serialisierungskonflikte zu erkennen, was CPU-Ressourcen verbraucht.
- Speicher-Overhead (MVCC): MVCC-Systeme speichern im Allgemeinen mehrere Versionen von Zeilen, was mehr Speicherplatz verbraucht und die I/O-Operationen für die Garbage Collection alter Versionen erhöhen kann.
- Transaktionsabbrüche/Wiederholungsversuche: Während MVCC die Blockierung reduziert, bricht die Commit-Zeit-Validierung für
-
Skalierbarkeitsprobleme:
- Hot Spots: Datenelemente, auf die von vielen Transaktionen häufig zugegriffen oder die von ihnen modifiziert werden, werden zu "Hot Spots". Unter
Serializable
-Isolation werden diese Hot Spots zu schwerwiegenden Engpässen, da Transaktionen gezwungen sind, auf den Zugriff zu warten, was die Skalierbarkeit erheblich einschränkt. - Globale Sperren: In einigen verteilten Datenbankarchitekturen kann die Erreichung globaler Serialisierbarkeit globale Koordinations- und Sperrmechanismen beinhalten, die noch größere Overheads und Latenzen einführen.
- Hot Spots: Datenelemente, auf die von vielen Transaktionen häufig zugegriffen oder die von ihnen modifiziert werden, werden zu "Hot Spots". Unter
Anwendungsfälle
Trotz des Leistungsoverheads ist die Serializable
-Isolation in bestimmten Szenarien unerlässlich, in denen absolute Datenkonsistenz nicht verhandelbar ist.
- Finanztransaktionen: Bankensysteme, Börsen und Zahlungs-Gateways erfordern perfekte Genauigkeit. Eine einzige falsche Summe oder Überweisung kann schwerwiegende finanzielle Folgen haben. Zum Beispiel erfordert die Sicherstellung, dass die Summe aller Konten niemals geändert wird, selbst bei gleichzeitigen Abhebungen und Einzahlungen,
Serializable
-Isolation. - Bestandsverwaltung: In Systemen, in denen die Lagerbestände exakt stimmen müssen, um Über- oder Unterverkäufe zu verhindern, insbesondere bei Artikeln mit begrenztem Bestand. Wenn zwei Kunden gleichzeitig versuchen, den letzten Artikel zu kaufen, sorgt
Serializable
dafür, dass nur einer erfolgreich ist. - Kritische Audits und Berichte: Wenn Berichte oder Audits eine Datenbankmomentaufnahme erfordern, die garantiert konsistent ist, als ob keine gleichzeitige Aktivität stattgefunden hätte, ist
Serializable
die einzig sichere Wahl. - Batch-Verarbeitung mit komplexen Abhängigkeiten: Bei komplexen Batch-Jobs, die das Lesen großer Datensätze, das Durchführen von Berechnungen und das Schreiben von Updates beinhalten, ist die Sicherstellung, dass alle Zwischenzustände konsistent sind und während des Prozesses keine "Phantomdaten" erscheinen, von entscheidender Bedeutung.
Beispielcode (PostgreSQL mit SSI)
Lassen Sie uns einen möglichen Serialisierungskonflikt und einen Wiederholungsmechanismus anhand einer einfachen Python-Anwendung und PostgreSQL veranschaulichen, die Serializable Snapshot Isolation verwendet.
Datenbankeinrichtung:
CREATE TABLE products ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, stock INT NOT NULL ); INSERT INTO products (name, stock) VALUES ('Laptop', 10); INSERT INTO products (name, stock) VALUES ('Mouse', 20);
Python-Anwendung (mit psycopg2
):
import psycopg2 from psycopg2.errors import SerializationFailure import time import threading DATABASE_URL = "dbname=test user=postgres password=root" def buy_product(product_id, quantity, thread_id): retries = 0 while retries < 5: # Erlaubt mehrere Wiederholungsversuche conn = None try: conn = psycopg2.connect(DATABASE_URL) conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE) cur = conn.cursor() print(f"Thread {thread_id}: Versuch, {quantity} von Produkt {product_id} zu kaufen (Wiederholung {retries})") # Simuliert eine Verarbeitungszeit time.sleep(0.1) cur.execute("SELECT stock FROM products WHERE id = %s FOR NO KEY UPDATE", (product_id,)) # FOR NO KEY UPDATE für gemeinsame Sperre der Zeilen, hilft bei der Serialisierung current_stock = cur.fetchone()[0] if current_stock >= quantity: new_stock = current_stock - quantity cur.execute("UPDATE products SET stock = %s WHERE id = %s", (new_stock, product_id)) conn.commit() print(f"Thread {thread_id}: Erfolgreich {quantity} von Produkt {product_id} gekauft. Neuer Bestand: {new_stock}") return True else: print(f"Thread {thread_id}: Nicht genügend Bestand für Produkt {product_id}. Aktuell: {current_stock}") conn.rollback() return False except SerializationFailure as e: print(f"Thread {thread_id}: SerializationFailure erkannt. Wiederholung... Fehler: {e}") if conn: conn.rollback() retries += 1 time.sleep(0.5) # Wartezeit vor Wiederholung, um sofortige erneute Kollision zu vermeiden except Exception as e: print(f"Thread {thread_id}: Ein unerwarteter Fehler ist aufgetreten: {e}") if conn: conn.rollback() return False finally: if conn: conn.close() print(f"Thread {thread_id}: Produkt {product_id} konnte nach {retries} Wiederholungsversuchen nicht gekauft werden.") return False # Simulierung gleichzeitiger Käufe if __name__ == "__main__"== "__main__": initial_stock = 0 conn_check = None try: conn_check = psycopg2.connect(DATABASE_URL) cur_check = conn_check.cursor() cur_check.execute("SELECT stock FROM products WHERE id = 1") initial_stock = cur_check.fetchone()[0] print(f"Anfänglicher Bestand für Produkt 1: {initial_stock}") finally: if conn_check: conn_check.close() threads = [] # Zwei Threads versuchen, jeweils 6 Laptops aus einem Anfangsbestand von 10 zu kaufen t1 = threading.Thread(target=buy_product, args=(1, 6, 1)) t2 = threading.Thread(target=buy_product, args=(1, 6, 2)) threads.append(t1) threads.append(t2) for t in threads: t.start() for t in threads: t.join() # Endbestand prüfen final_stock = 0 try: conn_check = psycopg2.connect(DATABASE_URL) cur_check = conn_check.cursor() cur_check.execute("SELECT stock FROM products WHERE id = 1") final_stock = cur_check.fetchone()[0] print(f"Endbestand für Produkt 1: {final_stock}") finally: if conn_check: conn_check.close() # Assert, dass der Bestand korrekt gehandhabt wird # Erwartet: Eine Transaktion wird erfolgreich ausgeführt (6 Artikel), die andere schlägt fehl oder versucht es erneut und schlägt fehl # Anfang: 10. Erwartetes Ende: 4 (wenn eine erfolgreich ist) oder 10 (wenn beide fehlschlagen oder nur eine teilweise erfolgreich ist und dann die andere fehlschlägt) # Wichtig ist, dass wir niemals -2 erhalten (10 - 6 - 6) expected_stocks_if_one_succeeds = initial_stock - 6 if final_stock == expected_stocks_if_one_succeeds: print("Bestand korrekt verwaltet: Eine Transaktion wurde erfolgreich verarbeitet.") elif final_stock == initial_stock: print("Bestand unverändert: Beide Transaktionen konnten aufgrund unzureichenden Bestands oder Konflikten nicht vollständig abgeschlossen werden.") else: print("Unerwarteter Endbestand. Überprüfen Sie die Logik oder den Datenbankstatus.")
In diesem Beispiel versuchen zwei Threads (t1
und t2
) 6 Einheiten von Produkt 1 mit einem Anfangsbestand von 10 zu kaufen. Mit Serializable
-Isolation in PostgreSQL (SSI) starten beide Transaktionen. Beide lesen den Bestand als 10.
- Wenn
t1
zuerst bestätigt wird, wird der Bestand auf 4 reduziert. Wennt2
dann versucht zu bestätigen (nachdem es 10 gelesen hat und auf 4 aktualisieren möchte), tritt wahrscheinlich eineSerializationFailure
auf, dat2
s Lesung vonstock=10
nun inkonsistent mitt1
s Schreibvorgang ist.t2
wird abgebrochen und muss sich wiederholen. Bei der Wiederholung liestt2
den Bestand als 4 und ermittelt korrekt einen unzureichenden Lagerbestand. - Es ist auch möglich, dass beide Transaktionen gleichzeitig versuchen zu aktualisieren, was zu einem Konflikt während des Commits führt.
Der entscheidende Punkt ist, dass der endgültige
stock
niemals negativ wird (z. B. 10 - 6 - 6 = -2), was ohne ordnungsgemäße Isolierung passieren würde. Eine Transaktion wird erfolgreich sein (Bestand wird auf 4 reduziert), und die andere wird aufgrund von unzureichendem Bestand fehlschlagen (nach einem Wiederholungsversuch, falls nicht sofort).
Schlussfolgerung
Die Serializable
-Isolation ist der ultimative Schutz für die Datenkonsistenz und garantiert, dass gleichzeitige Transaktionen die gleichen Ergebnisse liefern wie eine serielle Ausführung. Sie verhindert sorgfältig alle gängigen Nebenläufigkeitsanomalien und macht sie unverzichtbar für Anwendungen, bei denen finanzielle Genauigkeit, präzise Bestandsverwaltung oder strikte Datenintegrität von größter Bedeutung sind. Diese absolute Konsistenz geht jedoch mit erheblichen Kosten einher: reduzierter Durchsatz, erhöhte Latenz, höherer Ressourcenverbrauch und die Komplexität der Handhabung von Transaktionswiederholungen. Daher ist sie ein mächtiges Werkzeug, das mit Bedacht eingesetzt werden sollte; ihre Anwendung sollte denjenigen kritischen Operationen vorbehalten sein, bei denen die Kosten der Dateninkonsistenz die Leistungseinbußen bei weitem überwiegen und andere, leichtere Isolationsebenen nicht ausreichen.