Nachdem wir
Isolationsprobleme besprochen und einen Exkurs in Bezug auf die
Datenstruktur auf
niedriger Ebene gemacht hatten , untersuchten wir das letzte Mal
Zeilenversionen und beobachteten, wie verschiedene Operationen Tupel-Header-Felder veränderten.
Nun werden wir untersuchen, wie konsistente Datenschnappschüsse von Tupeln erhalten werden.
Was ist ein Datenschnappschuss?
Datenseiten können physisch mehrere Versionen derselben Zeile enthalten. Jede Transaktion darf jedoch nur eine (oder keine) Version jeder Zeile enthalten, damit alle zu einem bestimmten Zeitpunkt ein konsistentes Bild der Daten (im Sinne von ACID) ergeben.
Die Isolation in PosgreSQL basiert auf Snapshots: Jede Transaktion verwendet einen eigenen Daten-Snapshot, der Daten enthält, die vor der Erstellung des Snapshots festgeschrieben wurden, und keine Daten enthält, die zu diesem Zeitpunkt noch nicht festgeschrieben wurden. Wir haben
bereits gesehen , dass die resultierende Isolation zwar strenger als vom Standard gefordert erscheint, aber immer noch Anomalien aufweist.
Auf der Isolationsstufe Read Committed wird zu Beginn jeder Transaktionsanweisung ein Snapshot erstellt. Dieser Schnappschuss ist aktiv, während die Anweisung ausgeführt wird. In der Abbildung ist der Moment der Erstellung des Snapshots (der, wie wir uns erinnern, durch die Transaktions-ID bestimmt wird) blau dargestellt.

Auf den Ebenen Repeatable Read und Serializable wird der Snapshot einmal am Anfang der ersten Transaktionsanweisung erstellt. Ein solcher Schnappschuss bleibt bis zum Ende der Transaktion aktiv.

Sichtbarkeit von Tupeln in einem Schnappschuss
Sichtbarkeitsregeln
Ein Snapshot ist sicherlich keine physische Kopie aller notwendigen Tupel. Ein Snapshot wird tatsächlich durch mehrere Zahlen angegeben, und die Sichtbarkeit von Tupeln in einem Snapshot wird durch Regeln bestimmt.
Ob ein Tupel in einem Snapshot sichtbar ist oder nicht, hängt von zwei Feldern in der Kopfzeile ab, nämlich
xmin
und
xmax
,
xmax
den IDs der Transaktionen, mit denen das Tupel erstellt und gelöscht wurde. Intervalle wie dieses überlappen sich nicht und daher repräsentiert nicht mehr als eine Version eine Zeile in jedem Snapshot.
Die genauen Sichtbarkeitsregeln sind ziemlich kompliziert und berücksichtigen viele verschiedene Fälle und Extreme.
Sie können dies auf einfache Weise sicherstellen, indem Sie in src / backend / utils / time / tqual.c nachsehen (in Version 12 wurde das Häkchen nach src / backend / access / heap / heapam_visibility.c verschoben).
Der
xmin
können wir sagen, dass ein Tupel sichtbar ist, wenn im Snapshot die von der Transaktion
xmin
vorgenommenen Änderungen sichtbar sind, während die von der Transaktion
xmax
vorgenommenen
xmax
nicht sichtbar sind (mit anderen Worten, es ist bereits klar, dass das Tupel erstellt wurde, aber es ist noch nicht klar, ob es gelöscht wurde).
In Bezug auf eine Transaktion sind ihre Änderungen im Snapshot sichtbar, entweder wenn es genau die Transaktion ist, die den Snapshot erstellt hat (es werden die eigenen noch nicht festgeschriebenen Änderungen angezeigt), oder die Transaktion wurde festgeschrieben, bevor der Snapshot erstellt wurde.
Wir können Transaktionen nach Segmenten grafisch darstellen (von der Startzeit bis zur Festschreibungszeit):

Hier:
- Änderungen der Transaktion 2 sind sichtbar, seit sie abgeschlossen wurden, bevor der Snapshot erstellt wurde.
- Änderungen der Transaktion 1 sind nicht sichtbar, da sie zum Zeitpunkt der Erstellung des Snapshots aktiv war.
- Änderungen der Transaktion 3 sind nicht sichtbar, da sie nach dem Erstellen des Snapshots gestartet wurde (unabhängig davon, ob sie abgeschlossen wurde oder nicht).
Leider ist dem System der Festschreibungszeitpunkt von Transaktionen nicht bekannt. Es ist nur die Startzeit bekannt (die durch die Transaktions-ID bestimmt und in den obigen Abbildungen mit einer gestrichelten Linie gekennzeichnet ist), der Abschluss wird jedoch nirgendwo geschrieben.
Alles, was wir tun können, ist, den
aktuellen Status der Transaktionen bei der Snapshot-Erstellung herauszufinden. Diese Informationen stehen im gemeinsamen Speicher des Servers in der ProcArray-Struktur zur Verfügung, die die Liste aller aktiven Sitzungen und ihrer Transaktionen enthält.
Wir können jedoch post factum nicht herausfinden, ob eine bestimmte Transaktion zum Zeitpunkt der Erstellung des Snapshots aktiv war oder nicht. Daher muss in einem Snapshot eine Liste aller derzeit aktiven Transaktionen gespeichert werden.
Aus dem oben Gesagten folgt, dass es in PostgreSQL nicht möglich ist, einen Snapshot zu erstellen, der ab einem bestimmten Zeitpunkt konsistente Daten anzeigt,
selbst wenn alle erforderlichen Tupel auf Tabellenseiten verfügbar sind. Oft stellt sich die Frage, warum es in PostgreSQL an retrospektiven (oder zeitlichen oder Flashback-, wie Oracle sie nennt) Abfragen mangelt - und dies ist einer der Gründe.
Ein bisschen komisch ist, dass diese Funktionalität zuerst verfügbar war, dann aber aus dem DBMS gelöscht wurde. Lesen Sie dazu den Artikel von Joseph M. Hellerstein .
Der Schnappschuss wird also von mehreren Parametern bestimmt:
- In dem Moment, in dem der Snapshot erstellt wurde, genauer gesagt, die ID der nächsten Transaktion, die im System noch nicht verfügbar ist (
snapshot.xmax
). - Die Liste der aktiven (in Bearbeitung befindlichen) Transaktionen zum Zeitpunkt der Erstellung des Snapshots (
snapshot.xip
).
Zur Vereinfachung und Optimierung wird auch die ID der frühesten aktiven Transaktion gespeichert (
snapshot.xmin
). Dieser Wert ergibt einen wichtigen Sinn, auf den weiter unten eingegangen wird.
Der Schnappschuss speichert auch einige weitere Parameter, die für uns jedoch unwichtig sind.

Beispiel
Um zu verstehen, wie der Schnappschuss die Sichtbarkeit bestimmt, reproduzieren wir das obige Beispiel mit drei Transaktionen. Die Tabelle wird drei Zeilen haben, wobei:
- Die erste wurde von einer Transaktion hinzugefügt, die vor der Snapshot-Erstellung gestartet, danach jedoch abgeschlossen wurde.
- Die zweite wurde durch eine Transaktion hinzugefügt, die vor der Snapshot-Erstellung gestartet und abgeschlossen wurde.
- Die dritte wurde nach der Snapshot-Erstellung hinzugefügt.
=> TRUNCATE TABLE accounts;
Die erste Transaktion (noch nicht abgeschlossen):
=> BEGIN; => INSERT INTO accounts VALUES (1, '1001', 'alice', 1000.00); => SELECT txid_current();
=> SELECT txid_current(); txid_current -------------- 3695 (1 row)
Die zweite Transaktion (abgeschlossen, bevor der Snapshot erstellt wurde):
| => BEGIN; | => INSERT INTO accounts VALUES (2, '2001', 'bob', 100.00); | => SELECT txid_current();
| txid_current | -------------- | 3696 | (1 row)
| => COMMIT;
Erstellen eines Snapshots in einer Transaktion in einer anderen Sitzung.
|| => BEGIN ISOLATION LEVEL REPEATABLE READ; || => SELECT xmin, xmax, * FROM accounts;
|| xmin | xmax | id | number | client | amount || ------+------+----+--------+--------+-------- || 3696 | 0 | 2 | 2001 | bob | 100.00 || (1 row)
Festschreiben der ersten Transaktion nach dem Erstellen des Snapshots:
=> COMMIT;
Und die dritte Transaktion (erschienen, nachdem der Schnappschuss erstellt wurde):
| => BEGIN; | => INSERT INTO accounts VALUES (3, '2002', 'bob', 900.00); | => SELECT txid_current();
| txid_current | -------------- | 3697 | (1 row)
| => COMMIT;
Offensichtlich ist in unserem Schnappschuss nur noch eine Zeile sichtbar:
|| => SELECT xmin, xmax, * FROM accounts;
|| xmin | xmax | id | number | client | amount || ------+------+----+--------+--------+-------- || 3696 | 0 | 2 | 2001 | bob | 100.00 || (1 row)
Die Frage ist, wie Postgres das versteht.
Alles wird durch den Schnappschuss bestimmt. Schauen wir es uns an:
|| => SELECT txid_current_snapshot();
|| txid_current_snapshot || ----------------------- || 3695:3697:3695 || (1 row)
Hier werden
snapshot.xmin
,
snapshot.xmax
und
snapshot.xip
aufgelistet, die durch einen Doppelpunkt getrennt sind (
snapshot.xip
ist in diesem Fall eine Zahl, im Allgemeinen jedoch eine Liste).
Gemäß den obigen Regeln müssen im Snapshot die Änderungen sichtbar sein, die von Transaktionen mit den IDs
xid
, sodass
snapshot.xmin <= xid < snapshot.xmax
Ausnahme derjenigen, die in der Liste
snapshot.xip
. Sehen wir uns alle Tabellenzeilen an (im neuen Snapshot):
=> SELECT xmin, xmax, * FROM accounts ORDER BY id;
xmin | xmax | id | number | client | amount ------+------+----+--------+--------+--------- 3695 | 0 | 1 | 1001 | alice | 1000.00 3696 | 0 | 2 | 2001 | bob | 100.00 3697 | 0 | 3 | 2002 | bob | 900.00 (3 rows)
Die erste Zeile ist nicht sichtbar: Sie wurde von einer Transaktion in der Liste der aktiven Transaktionen erstellt (
xip
).
Die zweite Zeile ist sichtbar: Sie wurde von einer Transaktion erstellt, die sich im Snapshot-Bereich befindet.
Die dritte Zeile ist nicht sichtbar: Sie wurde von einer Transaktion erstellt, die außerhalb des Snapshot-Bereichs liegt.
|| => COMMIT;
Änderungen der Transaktion
Das Ermitteln der Sichtbarkeit der eigenen Änderungen der Transaktion erschwert die Situation etwas. In diesem Fall muss möglicherweise nur ein Teil dieser Änderungen angezeigt werden. Beispiel: Auf jeder Isolationsstufe darf ein zu einem bestimmten Zeitpunkt geöffneter Cursor keine Änderungen mehr sehen, die später vorgenommen werden.
Zu diesem Zweck verfügt ein Tupel-Header über ein spezielles Feld (dargestellt in den
cmin
und
cmax
), das die Auftragsnummer innerhalb der Transaktion anzeigt.
cmin
ist die Nummer zum Einfügen und
cmax
- zum Löschen, aber um Platz im
cmax
zu sparen, ist dies eigentlich ein Feld und nicht zwei verschiedene. Es wird angenommen, dass eine Transaktion selten dieselbe Zeile einfügt und löscht.
In diesem Fall wird eine spezielle Kombinationsbefehls-ID (
combocid
) in dasselbe Feld eingefügt, und der
cmin
cmax
Prozess merkt sich die tatsächlichen
cmax
für
cmin
und
cmax
für diese
combocid
. Das ist aber ganz exotisch.
Hier ist ein einfaches Beispiel. Beginnen wir eine Transaktion und fügen der Tabelle eine Zeile hinzu:
=> BEGIN; => SELECT txid_current();
txid_current -------------- 3698 (1 row)
INSERT INTO accounts(id, number, client, amount) VALUES (4, 3001, 'charlie', 100.00);
Lassen Sie uns den Inhalt der Tabelle zusammen mit dem Feld
cmin
(aber nur für Zeilen, die von der Transaktion hinzugefügt wurden - für andere ist dies bedeutungslos):
=> SELECT xmin, CASE WHEN xmin = 3698 THEN cmin END cmin, * FROM accounts;
xmin | cmin | id | number | client | amount ------+------+----+--------+---------+--------- 3695 | | 1 | 1001 | alice | 1000.00 3696 | | 2 | 2001 | bob | 100.00 3697 | | 3 | 2002 | bob | 900.00 3698 | 0 | 4 | 3001 | charlie | 100.00 (4 rows)
Jetzt öffnen wir einen Cursor für eine Abfrage, die die Anzahl der Zeilen in der Tabelle zurückgibt.
=> DECLARE c CURSOR FOR SELECT count(*) FROM accounts;
Und danach fügen wir eine weitere Zeile hinzu:
=> INSERT INTO accounts(id, number, client, amount) VALUES (5, 3002, 'charlie', 200.00);
Die Abfrage gibt 4 zurück - die nach dem Öffnen des Cursors hinzugefügte Zeile gelangt nicht in den Datenschnappschuss:
=> FETCH c;
count ------- 4 (1 row)
Warum? Weil der Snapshot nur Tupel mit
cmin < 1
berücksichtigt.
=> SELECT xmin, CASE WHEN xmin = 3698 THEN cmin END cmin, * FROM accounts;
xmin | cmin | id | number | client | amount ------+------+----+--------+---------+--------- 3695 | | 1 | 1001 | alice | 1000.00 3696 | | 2 | 2001 | bob | 100.00 3697 | | 3 | 2002 | bob | 900.00 3698 | 0 | 4 | 3001 | charlie | 100.00 3698 | 1 | 5 | 3002 | charlie | 200.00 (5 rows)
=> ROLLBACK;
Ereignishorizont
Die ID der frühesten aktiven Transaktion (
snapshot.xmin
) hat einen wichtigen Sinn: Sie bestimmt den "Ereignishorizont" der Transaktion. Das heißt, über den Horizont hinaus werden für die Transaktion immer nur aktuelle Zeilenversionen angezeigt.
Tatsächlich muss eine veraltete (Dead) Row-Version nur sichtbar sein, wenn die aktuelle Version durch eine noch nicht abgeschlossene Transaktion erstellt wurde und daher noch nicht sichtbar ist. Aber alle Transaktionen "jenseits des Horizonts" sind mit Sicherheit abgeschlossen.

Den Transaktionshorizont sehen Sie im Systemkatalog:
=> BEGIN; => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
backend_xmin -------------- 3699 (1 row)
Wir können den Horizont auch auf Datenbankebene definieren. Dazu müssen wir alle aktiven Schnappschüsse machen und die ältesten
xmin
unter ihnen finden. Und es definiert den Horizont, ab dem tote Tupel in der Datenbank für keine Transaktion sichtbar sind.
Solche Tupel können abgesaugt werden - und genau deshalb ist das Konzept des Horizonts aus praktischer Sicht so wichtig.
Wenn eine bestimmte Transaktion einen Snapshot für längere Zeit enthält, wird dadurch auch der Datenbankhorizont beibehalten. Darüber hinaus wird nur das Vorhandensein einer nicht abgeschlossenen Transaktion den Horizont bestimmen, auch wenn die Transaktion selbst nicht den Snapshot enthält.
Und das bedeutet, dass tote Tupel in der DB nicht weggesaugt werden können. Darüber hinaus ist es möglich, dass sich eine "Long-Play" -Transaktion mit Daten überhaupt nicht mit anderen Transaktionen überschneidet, was jedoch keine Rolle spielt, da sich alle einen Datenbankhorizont teilen.
Wenn wir jetzt ein Segment so einrichten, dass es Snapshots (von
snapshot.xmin
bis
snapshot.xmax
) und keine Transaktionen darstellt, können wir die Situation wie folgt darstellen:

In dieser Abbildung bezieht sich der niedrigste Snapshot auf eine nicht abgeschlossene Transaktion, und in den anderen Snapshots kann
snapshot.xmin
nicht größer als die Transaktions-ID sein.
In unserem Beispiel wurde die Transaktion mit der Isolationsstufe Read Committed gestartet. Auch wenn kein aktiver Datenschnappschuss vorhanden ist, wird der Horizont beibehalten:
| => BEGIN; | => UPDATE accounts SET amount = amount + 1.00; | => COMMIT;
=> SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
backend_xmin -------------- 3699 (1 row)
Und erst nach Abschluss der Transaktion bewegt sich der Horizont vorwärts, wodurch tote Tupel abgesaugt werden können:
=> COMMIT; => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
backend_xmin -------------- 3700 (1 row)
In dem Fall, dass die beschriebene Situation wirklich Probleme verursacht und es keine Möglichkeit gibt, sie auf Anwendungsebene zu umgehen, stehen ab Version 9.6 zwei Parameter zur Verfügung:
old_snapshot_threshold
bestimmt die maximale Lebensdauer des Snapshots. Nach Ablauf dieser Zeit kann der Server tote Tupel staubsaugen, und wenn eine "Long-Play" -Transaktion diese noch benötigt, wird der Fehler "Snapshot zu alt" ausgegeben.idle_in_transaction_session_timeout
bestimmt die maximale Lebensdauer einer idle_in_transaction_session_timeout
Transaktion. Nach Ablauf dieser Zeit wird die Transaktion abgebrochen.
Schnappschussexport
Es kann vorkommen, dass mehrere gleichzeitige Transaktionen garantiert werden müssen, um dieselben Daten anzuzeigen. Ein Beispiel ist
pg_dump
Dienstprogramm
pg_dump
, das im parallelen Modus ausgeführt werden kann: Alle Arbeitsprozesse müssen die Datenbank im gleichen Status sehen, damit die Sicherungskopie konsistent ist.
Natürlich können wir uns nicht auf die Annahme verlassen, dass für die Transaktionen dieselben Daten angezeigt werden, nur weil sie "gleichzeitig" gestartet wurden. Zu diesem Zweck stehen Export und Import eines Snapshots zur Verfügung.
Die Funktion
pg_export_snapshot
gibt die Snapshot-ID zurück, die an eine andere Transaktion übergeben werden kann (mithilfe von Tools außerhalb des DBMS).
=> BEGIN ISOLATION LEVEL REPEATABLE READ; => SELECT count(*) FROM accounts;
count ------- 3 (1 row)
=> SELECT pg_export_snapshot();
pg_export_snapshot --------------------- 00000004-00000E7B-1 (1 row)
Die andere Transaktion kann den Snapshot mit dem Befehl SET TRANSACTION SNAPSHOT importieren, bevor die erste Abfrage ausgeführt wird. Die Isolationsstufe "Wiederholbares Lesen" oder "Serialisierbar" sollte ebenfalls angegeben werden, da Anweisungen auf der Ebene "Read Committed" ihre eigenen Snapshots verwenden.
| => DELETE FROM accounts; | => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SET TRANSACTION SNAPSHOT '00000004-00000E7B-1';
Die zweite Transaktion funktioniert jetzt mit dem Snapshot der ersten und sieht daher drei Zeilen (anstatt Null):
| => SELECT count(*) FROM accounts;
| count | ------- | 3 | (1 row)
Die Lebensdauer eines exportierten Snapshots entspricht der Lebensdauer der exportierenden Transaktion.
| => COMMIT; => COMMIT;
Lesen Sie weiter .