Tiefe Einblicke in MVCC in PostgreSQL
Grace Collins
Solutions Engineer · Leapcell

MVCC in PostgreSQL verstehen
Einführung
In der Welt der Datenbankverwaltungssysteme ist die Gewährleistung von Datenkonsistenz und -isolation bei gleichzeitiger Bearbeitung zahlreicher gleichzeitiger Operationen eine monumentale Herausforderung. Ohne robuste Mechanismen können gleichzeitige Transaktionen zu einer Vielzahl von Problemen führen: verlorene Aktualisierungen, schmutzige Lesevorgänge, nicht wiederholbare Lesevorgänge und Phantomlesevorgänge. Diese Probleme können Daten beschädigen, Anwendungslogik brechen und die Leistung stark beeinträchtigen. Hier werden Mechanismen zur Nebenläufigkeitskontrolle unverzichtbar. Unter ihnen sticht das Multi-Version Concurrency Control (MVCC) als ein hochwirksamer und weit verbreiteter Ansatz hervor, insbesondere in relationalen Datenbanken wie PostgreSQL. MVCC ermöglicht es verschiedenen Transaktionen, verschiedene "Schnappschüsse" der Datenbank zu sehen, wodurch die Notwendigkeit traditioneller Sperrmechanismen während Leseoperationen praktisch entfällt und dadurch die Nebenläufigkeit erheblich verbessert wird. Das Verständnis, wie MVCC in PostgreSQL funktioniert, ist entscheidend für jeden, der die Datenbankleistung optimieren, Nebenläufigkeitsprobleme beheben oder einfach eine tiefere Wertschätzung für das technische Wunderwerk eines modernen RDBMS gewinnen möchte. Dieser Artikel befasst sich mit den Feinheiten von MVCC in PostgreSQL, entschlüsselt seine Kernprinzipien und zeigt, wie es effiziente, nicht blockierende Operationen ermöglicht.
Kernkonzepte von MVCC
Bevor wir uns mit der spezifischen Implementierung von PostgreSQL befassen, wollen wir ein grundlegendes Verständnis der Schlüsselkonzepte erlangen, die MVCC zugrunde liegen.
- Transaktions-ID (XID): Jede Transaktion in PostgreSQL erhält eine eindeutige, monoton steigende 32-Bit-Transaktions-ID (XID). Diese XID ist für MVCC von grundlegender Bedeutung und markiert den Beginn einer Transaktion.
- Tupel-Versionen: Anstatt Datensätze direkt zu aktualisieren, erstellen MVCC-Systeme wie PostgreSQL neue Versionen (Tupel) einer Zeile, wann immer diese geändert oder gelöscht wird. Die alte Version bleibt in der Datenbank für andere gleichzeitige Transaktionen, die sie möglicherweise noch benötigen.
- Sichtbarkeitsregeln: Diese Regeln bestimmen, welche Tupelversion eine bestimmte Transaktion "sehen" kann. Die Sichtbarkeit eines Tupels wird anhand seiner Erstellungs-XID (
xmin
) und seiner Ablauf-XID (xmax
) im Verhältnis zur XID der aktuellen Transaktion beurteilt. xmin
(Erstellungs-XID): Dies ist die XID der Transaktion, die diese spezielle Version des Tupels erstellt hat.xmax
(Lösch-/Aktualisierungs-XID): Dies ist die XID der Transaktion, die diese Tupelversion "logisch" gelöscht oder aktualisiert hat (was effektiv bedeutet, eine neue Version zu erstellen und die alte als "gelöscht" durch diese Transaktion zu markieren). Wennxmax
0 ist, wurde das Tupel nicht gelöscht oder aktualisiert.- Transaktionsstatus: Neben aktiven XIDs pflegt PostgreSQL auch ein
pg_clog
(Commit-Protokoll), das den Status vergangener Transaktionen (committet, abgebrochen, noch in Bearbeitung) speichert. Dies ist für die korrekte Anwendung der Sichtbarkeitsregeln unerlässlich.
MVCC in der Praxis: Wie PostgreSQL funktioniert
Die MVCC-Implementierung von PostgreSQL dreht sich um diese Tupelversionen und Sichtbarkeitsregeln. Wenn eine Transaktion Daten lesen muss, erwirbt sie keine Sperren, die andere Schreiber blockieren würden. Stattdessen konsultiert sie die xmin
und xmax
jedes Tupels und den Transaktionsstatus, um festzustellen, welche Version für sie sichtbar ist.
Der Aktualisierungsvorgang
Betrachten wir eine einfache UPDATE
-Operation in einer Tabelle namens products
:
UPDATE products SET price = 100.00 WHERE id = 1;
Wenn diese Anweisung innerhalb einer Transaktion (sagen wir Transaktions-XID 100) ausgeführt wird, modifiziert PostgreSQL die vorhandene Zeile nicht direkt. Stattdessen führt es die folgenden Schritte aus:
- Markieren des alten Tupels: Es findet das vorhandene Tupel für
id = 1
und setzt dessenxmax
auf 100 (die XID der aktuellen Transaktion). Dies markiert effektiv die alte Version als "gelöscht" durch Transaktion XID 100. - Einfügen eines neuen Tupels: Es erstellt ein brandneues Tupel für
id = 1
mit dem neuenprice = 100.00
. Diexmin
dieses neuen Tupels wird auf 100 und seinexmax
auf 0 gesetzt (was bedeutet, dass es noch nicht gelöscht wurde).
Entscheidend ist, dass, wenn Transaktion XID 100 COMMITt, das xmax
des alten Tupels effektiv mit einer committeten Transaktion assoziiert wird und das xmin
des neuen Tupels ebenfalls mit einer committeten Transaktion assoziiert wird. Wenn Transaktion XID 100 ROLLBACKt, werden beide Änderungen effektiv rückgängig gemacht: Das xmax
des alten Tupels wird gelöscht und das neue Tupel wird unsichtbar und für die Garbage Collection berechtigt.
Sichtbarkeitsregeln
Eine Tupelversion ist für eine Transaktion (nennen wir sie Transaktion Y mit der XID current_xid
) nur dann sichtbar, wenn die folgenden Bedingungen erfüllt sind:
-
Erstellungs-XID (
xmin
) Prüfung:xmin
ist kleiner alscurrent_xid
.- ODER
xmin
ist gleichcurrent_xid
(d. h. die aktuelle Transaktion hat es erstellt). - UND
xmin
ist eine committete Transaktion (es sei denn,xmin
istcurrent_xid
).
-
Lösch-XID (
xmax
) Prüfung:xmax
ist 0 (d.h. es wurde noch nicht gelöscht).- ODER
xmax
ist größer alscurrent_xid
. - ODER
xmax
ist gleichcurrent_xid
(d.h. die aktuelle Transaktion hat es gelöscht, aber die alte Version könnte immer noch für diese Transaktion sichtbar sein, wenn sie ihre eigenen Änderungen liest). - UND
xmax
ist eine abgebrochene Transaktion (es sei denn,xmax
istcurrent_xid
und die aktuelle Transaktion hat es gelöscht).
Diese Regeln stellen sicher, dass jede Transaktion einen konsistenten Schnappschuss der Daten zum Zeitpunkt ihres Beginns sieht und so vor schmutzigen Lesevorgängen und nicht wiederholbaren Lesevorgängen geschützt ist.
Lassen Sie uns dies anhand eines Beispiels verdeutlichen:
-- Anfangszustand: -- products Tabelle: -- (id=1, name='Laptop', price=999.99, xmin=50, xmax=0) -- committet von XID 50 -- Transaktion A (XID 100) startet BEGIN TRANSACTION; -- Andere Transaktionen (XID 90, 95) lesen möglicherweise gleichzeitig. Sie sehen die Version von XID 50. -- Transaktion B (XID 105) startet, zielt darauf ab, den Laptop-Preis zu aktualisieren BEGIN TRANSACTION; UPDATE products SET price = 1050.00 WHERE id = 1; -- PostgreSQL-Zustand (vereinfacht): -- (id=1, name='Laptop', price=999.99, xmin=50, xmax=105) -- alte Version markiert von XID 105 -- (id=1, name='Laptop', price=1050.00, xmin=105, xmax=0) -- neue Version erstellt von XID 105 -- Innerhalb von Transaktion A (XID 100): SELECT * FROM products WHERE id = 1; -- Ausgabe: (id=1, name='Laptop', price=999.99) -- WARUM? Weil XID 100 die von XID 50 erstellte Version sieht (`xmin=50 < 100`, `xmax=105 > 100`, aber XID 105 ist noch in Bearbeitung oder noch nicht committet, wenn XID 100 sie liest). -- Die von XID 105 erstellte Version (`xmin=105 > 100`) ist für XID 100 nicht sichtbar. -- Transaktion B (XID 105) COMMITt COMMIT; -- Jetzt startet eine neue Transaktion C (XID 110) BEGIN TRANSACTION; SELECT * FROM products WHERE id = 1; -- Ausgabe: (id=1, name='Laptop', price=1050.00) -- WARUM? XID 110: -- - Für (xmin=50, xmax=105): xmin=50 < 110 (committet), xmax=105 < 110 (committet). Diese Version ist NICHT sichtbar, da sie von der committeten XID 105 gelöscht wurde. -- - Für (xmin=105, xmax=0): xmin=105 < 110 (committet), xmax=0. Diese Version IST sichtbar. COMMIT;
Die pg_clog
und der Transaktionsstatus
Das pg_clog
(auch bekannt als pg_xact
) ist eine entscheidende Komponente, die den Commit-Status vergangener Transaktionen speichert. Es ist ein Verzeichnis von Dateien, das XIDs ihren Zuständen (committet, abgebrochen, noch in Bearbeitung) zuordnet. Wenn eine Transaktion den Status einer xmin
oder xmax
überprüfen muss, konsultiert sie pg_clog
. Dies ermöglicht es PostgreSQL, schnell festzustellen, ob eine Transaktion (und damit eine Tupelversion) "live" ist oder nicht.
Indexverhalten mit MVCC
Indizes in PostgreSQL folgen ebenfalls den MVCC-Prinzipien. Ein Indexeintrag verweist auf eine bestimmte Tupelversion. Das bedeutet, wenn eine Zeile aktualisiert wird, kann der ursprüngliche Indexeintrag immer noch auf das alte Tupel verweisen (das jetzt mit xmax
markiert ist). Möglicherweise wird auch ein neuer Indexeintrag erstellt, der auf das neue Tupel verweist. Deshalb ist VACUUM
unerlässlich (wie im nächsten Abschnitt erläutert), um diese "toten" Einträge zu bereinigen.
Die Rolle von VACUUM
Eine bedeutende Folge der "neue Version schreiben"-Strategie von MVCC ist die Ansammlung von "toten Tupeln" – alte Versionen von Zeilen, die für keine aktive Transaktion mehr sichtbar sind. Wenn diese toten Tupel unkontrolliert bleiben, können sie die Datenbank aufblähen, Speicherplatz verbrauchen und die Abfrageleistung beeinträchtigen (da Indizes darauf verweisen könnten, was zusätzliche Prüfungen erfordert).
Hier kommt VACUUM
ins Spiel. VACUUM
ist der Garbage Collector von PostgreSQL. Seine Hauptaufgaben sind:
- Tote Tupel entfernen: Es identifiziert und gibt Speicherplatz frei, der von toten Tupeln belegt wird.
- Sichtbarkeitskarte aktualisieren: Es aktualisiert die Sichtbarkeitskarte, die Index-Only-Scans beschleunigt.
- Alte Transaktionen einfrieren: Es aktualisiert den Transaktions-ID-Zähler, um einen Transaktions-ID-Überlauf zu verhindern, ein kritisches Problem für langlaufende Datenbanken.
VACUUM FULL
führt eine aggressivere Neufassung der Tabelle durch, gibt Speicherplatz frei und schrumpft die Tabellendatei auf der Festplatte, erfordert jedoch eine exklusive Sperre und ist daher störender. AUTOVACUUM
ist ein Hintergrundprozess, der automatisch VACUUM
und ANALYZE
(Statistiksammlung) ausführt, um die Datenbank gesund zu halten.
MVCC-Konsistenzebenen (Isolationsstufen)
PostgreSQL implementiert MVCC, um verschiedene SQL-Isolationsstufen zu unterstützen:
- Read Committed (Standard): Jede Anweisung innerhalb einer Transaktion sieht einen frischen Schnappschuss der Datenbank. Wenn eine Transaktion
A=1
liest, dann eine andere Transaktion Änderungen committet, dieA=2
machen, und die erste TransaktionA
erneut liest, wird sieA=2
sehen. Dies verhindert schmutzige Lesevorgänge, aber keine nicht wiederholbaren Lesevorgänge oder Phantomlesevorgänge. - Repeatable Read: Bietet einen konsistenten Schnappschuss für die gesamte Transaktion. Alle Anweisungen innerhalb einer Transaktion beobachten die Datenbank so, wie sie zu Beginn dieser Transaktion war. Dies verhindert schmutzige Lesevorgänge und nicht wiederholbare Lesevorgänge.
- Serializable: Die höchste Isolationsstufe garantiert, dass die gleichzeitige Ausführung von Transaktionen dasselbe Ergebnis liefert, als ob sie seriell ausgeführt worden wären. PostgreSQL erreicht dies mit einer Technik namens "Serializable Snapshot Isolation" (SSI), die Transaktionen erkennt und abbricht, die möglicherweise Serialisierungsanomalien verursacht haben. Dies verhindert alle gängigen Nebenläufigkeitsprobleme, einschließlich Phantomlesevorgängen.
-- Beispiel demonstriert Read Committed vs. Repeatable Read -- Angenommen, die Tabelle 'accounts' hat (id INT, balance INT) -- Anfangszustand: (1, 1000) -- SESSION 1 (Read Committed) BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; -- Bei T1 SELECT balance FROM accounts WHERE id = 1; -- Gibt 1000 zurück -- SESSION 2 BEGIN TRANSACTION; UPDATE accounts SET balance = 1200 WHERE id = 1; COMMIT; -- Commit bei T2 -- Zurück zu SESSION 1 -- Bei T3 (nachdem T2 committet wurde) SELECT balance FROM accounts WHERE id = 1; -- Gibt 1200 zurück -- Dies ist ein nicht wiederholbarer Lesevorgang: dieselbe Abfrage hat unterschiedliche Ergebnisse innerhalb derselben Transaktion geliefert. COMMIT; -- SESSION 3 (Repeatable Read) BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- Bei T4 SELECT balance FROM accounts WHERE id = 1; -- Gibt 1000 zurück -- SESSION 4 BEGIN TRANSACTION; UPDATE accounts SET balance = 1500 WHERE id = 1; COMMIT; -- Commit bei T5 -- Zurück zu SESSION 3 -- Bei T6 (nachdem T5 committet wurde) SELECT balance FROM accounts WHERE id = 1; -- Gibt 1000 zurück -- Die Daten bleiben innerhalb der Transaktion konsistent, auch wenn andere Transaktionen Änderungen committen. COMMIT;
Fazit
MVCC ist das Fundament des Konkurrenzmodells von PostgreSQL und bietet eine robuste und effiziente Möglichkeit, mehrere Transaktionen ohne starke Abhängigkeit von traditionellen Sperren für Lesevorgänge zu verarbeiten. Durch die Erstellung neuer Tupelversionen anstelle der direkten Aktualisierung und die Anwendung ausgeklügelter Sichtbarkeitsregeln stellt PostgreSQL sicher, dass jede Transaktion auf einem konsistenten Schnappschuss der Daten operiert. Dieses Design verbessert nicht nur die Nebenläufigkeit, sondern bildet auch die Grundlage für seine starken Isolationsgarantien. Obwohl MVCC den Overhead für tote Tupel und die Notwendigkeit von VACUUM
mit sich bringt, machen die Vorteile in Bezug auf Leistung, Zuverlässigkeit und Datenintegrität es zu einer unverzichtbaren Technologie für moderne Datenbanksysteme. Die MVCC-Implementierung von PostgreSQL ist ein Beweis seiner technischen Exzellenz und ermöglicht hochgradig gleichzeitige Operationen bei gleichzeitiger Beibehaltung der ACID-Eigenschaften und Gewährleistung von Datenkonsistenz und Spitzenleistung für eine Vielzahl von Anwendungen.