MVCC-4. Datenschnappschüsse

Nachdem wir Probleme im Zusammenhang mit der Isolation untersucht und einen Exkurs über das Organisieren von Daten auf niedriger Ebene gemacht hatten , haben wir das letzte Mal ausführlich über Zeilenversionen gesprochen und nachverfolgt, wie sich die Serviceinformationen im Versionsheader während verschiedener Vorgänge ändern.

Heute schauen wir uns an, wie konsistente Versionen von Daten aus Zeilenversionen erhalten werden.

Was ist ein Datenschnappschuss?


Physisch können Datenseiten mehrere Versionen derselben Zeile enthalten. Darüber hinaus sollte jede Transaktion nur eine (oder keine) Version jeder Zeile sehen, damit sie zusammen ein ACID-konsistentes Bild der Daten zu einem bestimmten Zeitpunkt bilden.

Die Isolation in PostgreSQL basiert auf Snapshots: Jede Transaktion arbeitet mit einem eigenen Daten-Snapshot, der die Daten enthält, die vor der Erstellung des Snapshots aufgezeichnet wurden, und die zu diesem Zeitpunkt noch nicht festgelegten Daten nicht enthält. Wir haben bereits gesehen, dass die Isolation in diesem Fall strenger ist als es der Standard erfordert, jedoch nicht ohne Anomalien.

Auf der Isolationsstufe von Read Committed wird zu Beginn jeder Transaktionsanweisung ein Snapshot erstellt. Ein solcher Snapshot ist aktiv, während die Anweisung ausgeführt wird. In der Abbildung ist der Zeitpunkt der Erstellung des Schnappschusses (der, wie wir uns erinnern, durch die Transaktionsnummer bestimmt wird) blau dargestellt.



Auf der Ebene "Wiederholbares Lesen" und "Serialisierbar" wird zu Beginn der ersten Transaktionsanweisung einmal ein Snapshot erstellt. Ein solcher Snapshot bleibt bis zum Ende der Transaktion aktiv.



Sichtbarkeit von Zeilenversionen im Snapshot


Sichtbarkeitsregeln


Ein Snapshot ist natürlich keine physische Kopie aller erforderlichen Zeilenversionen. Tatsächlich wird ein Snapshot durch mehrere Zahlen angegeben, und die Sichtbarkeit der Zeilenversionen im Snapshot wird durch die Regeln bestimmt.

Ob diese Version der Zeile im Snapshot sichtbar ist oder nicht, hängt von den beiden Feldern des Headers - xmin und xmax - ab, dh von der Anzahl der Transaktionen, die sie erstellt und gelöscht haben. Solche Intervalle schneiden sich nicht, daher wird eine Linie in einem Bild durch maximal eine ihrer Versionen dargestellt.

Die genauen Sichtbarkeitsregeln sind recht komplex und berücksichtigen viele verschiedene Situationen und Extremfälle.
Dies kann leicht überprüft werden, indem Sie sich src / backend / utils / time / tqual.c ansehen (in Version 12 wurde die Prüfung nach src / backend / access / heap / heapam_visibility.c verschoben).

Zur Vereinfachung können wir sagen, dass die Version der Zeile sichtbar ist, wenn die durch die xmin-Transaktion vorgenommenen Änderungen im Bild sichtbar sind und die durch die xmax-Transaktion vorgenommenen Änderungen nicht sichtbar sind (mit anderen Worten, es ist bereits sichtbar, dass die Version der Zeile angezeigt wurde, aber es ist noch nicht sichtbar, dass sie gelöscht wurde).

Transaktionsänderungen sind wiederum im Snapshot sichtbar, wenn dies entweder dieselbe Transaktion ist, die den Snapshot erstellt hat (sie sieht ihre eigenen Änderungen), oder wenn die Transaktion festgeschrieben wurde, bevor der Snapshot erstellt wurde.

Es ist möglich, Transaktionen grafisch in Form von Segmenten darzustellen (vom Start bis zum Zeitpunkt des Festschreibens):



Hier:

  • Änderungen an Transaktion 2 werden angezeigt, da sie abgeschlossen wurden, bevor der Snapshot erstellt wurde.
  • Änderungen an Transaktion 1 sind nicht sichtbar, da sie zum Zeitpunkt der Erstellung des Snapshots aktiv waren.
  • Änderungen an Transaktion 3 sind nicht sichtbar, da sie nach der Erstellung des Snapshots gestartet wurden (es spielt keine Rolle, ob er beendet wurde oder nicht).

Leider ist der Zeitpunkt des Festschreibens von Transaktionen dem System unbekannt. Es ist nur der Zeitpunkt seines Beginns bekannt (er wird durch die Transaktionsnummer bestimmt und in den obigen Abbildungen durch eine gestrichelte Linie angezeigt), aber die Tatsache der Fertigstellung wird nirgendwo aufgezeichnet.

Wir können lediglich den aktuellen Status von Transaktionen ermitteln, wenn wir einen Snapshot erstellen. Diese Informationen befinden sich im gemeinsam genutzten Speicher des Servers in der ProcArray-Struktur, die eine Liste aller aktiven Sitzungen und ihrer Transaktionen enthält.

Und im Nachhinein können wir nicht mehr verstehen, ob zum Zeitpunkt der Erstellung des Snapshots eine Transaktion aktiv war oder nicht. Daher muss die Liste aller derzeit aktiven Transaktionen im Bild gespeichert werden.

Daraus folgt, dass Sie in PostgreSQL keinen Snapshot erstellen können, der konsistente Daten zu einem beliebigen Zeitpunkt zeigt, selbst wenn alle dafür erforderlichen Versionen der Zeilen auf den Tabellenseiten vorhanden sind. Man hört oft die Frage, warum es in PostgreSQL keine retrospektiven (oder zeitlichen; in Oracle wird dies als Flashback-Abfrage bezeichnet) Abfragen gibt - dies ist einer der Gründe.
Es ist lustig, dass es anfangs solche Funktionen gab, aber später wurde sie aus dem DBMS entfernt. Sie können darüber in einem Artikel von Joseph Hellerstein lesen.
Ein Datenschnappschuss wird also durch mehrere Parameter bestimmt:

  • der Zeitpunkt der Erstellung des Snapshots, nämlich die Nummer der nächsten Transaktion, die nicht im System vorhanden ist ( snapshot.xmax );
  • Eine Liste der aktiven Transaktionen zum Zeitpunkt der Erstellung des Snapshots ( snapshot.xip ).

Zur Vereinfachung und Optimierung wird die Nummer der frühesten aktiven Transaktion ( snapshot.xmin ) ebenfalls separat gespeichert. Dieser Wert hat eine wichtige Bedeutung, die wir unten diskutieren werden.

Außerdem werden einige weitere Parameter im Bild gespeichert, die für uns jedoch nicht wichtig sind.



Beispiel


Um zu sehen, wie die Sichtbarkeit durch den Schnappschuss bestimmt wird, reproduzieren wir die Situation mit den drei oben diskutierten Transaktionen. Die Tabelle besteht aus drei Zeilen mit:

  • Die erste wurde durch eine Transaktion hinzugefügt, die vor der Erstellung des Snapshots gestartet und später beendet wurde.
  • Die zweite wird durch eine Transaktion hinzugefügt, die vor dem Erstellen des Snapshots gestartet und beendet wurde.
  • Der dritte wurde nach der Aufnahme hinzugefügt.

=> TRUNCATE TABLE accounts; 

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 Sie einen Snapshot 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) 

Wir schließen die erste Transaktion ab, nachdem der Snapshot erstellt wurde:

 => COMMIT; 

Und die dritte Transaktion (erschien später im Schnappschuss):

 | => BEGIN; | => INSERT INTO accounts VALUES (3, '2002', 'bob', 900.00); | => SELECT txid_current(); 
 | txid_current | -------------- | 3697 | (1 row) 
 | => COMMIT; 

Offensichtlich ist in unserem Bild noch eine Linie 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 PostgreSQL dies versteht.

Alles wird vom Bild bestimmt. Schauen wir es uns an:

 || => SELECT txid_current_snapshot(); 
 || txid_current_snapshot || ----------------------- || 3695:3697:3695 || (1 row) 

Hier listet der Doppelpunkt snapshot.xmin, snapshot.xmax und snapshot.xip auf (in diesem Fall eine Nummer, aber im Allgemeinen eine Liste).

Gemäß den oben angegebenen Regeln sollte das Bild die Änderungen zeigen, die durch Transaktionen mit den Nummern snapshot.xmin <= xid <snapshot.xmax mit Ausnahme von snapshot.xip vorgenommen wurden. Schauen wir uns alle Zeilen der Tabelle an (in einem neuen Bild):

 => 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 erstellt, die in der Liste der aktiven (xip) enthalten ist.
Die zweite Zeile ist sichtbar - sie wird durch eine Transaktion erstellt, die in den Bereich des Bildes fällt.
Die dritte Zeile ist nicht sichtbar - sie wurde von einer Transaktion erstellt, die nicht im Bereich des Snapshots liegt.

 || => COMMIT; 

Eigene Änderungen


Etwas komplizierter ist das Bild, wenn Sie die Sichtbarkeit Ihrer eigenen Transaktionsänderungen bestimmen. Hier müssen Sie möglicherweise nur einen Teil dieser Änderungen sehen. Beispielsweise sollte ein Cursor, der zu einem bestimmten Zeitpunkt geöffnet ist, keine Änderungen sehen, die nach diesem Moment in einer Isolationsstufe vorgenommen wurden.

Zu diesem Zweck befindet sich im Header der Zeilenversion ein spezielles Feld (das in den Pseudospalten cmin und cmax angezeigt wird), in dem die Sequenznummer der Operation innerhalb der Transaktion angezeigt wird. Cmin steht für die einzufügende Zahl, cmax für die zu löschende Zahl. Um jedoch Platz in der Zeilenüberschrift zu sparen, ist dies tatsächlich ein Feld, nicht zwei verschiedene. Es wird angenommen, dass das Einfügen und Löschen derselben Zeile in einer einzelnen Transaktion selten ist.

Wenn dies immer noch passiert, wird eine spezielle "Combo" -Nummer in dasselbe Feld eingefügt, über die sich der Serviceprozess die tatsächlichen cmin und cmax merkt. Das ist aber völlig exotisch.

Ein einfaches Beispiel. Wir starten die Transaktion und fügen die Zeile zur Tabelle hinzu:

 => BEGIN; => SELECT txid_current(); 
  txid_current -------------- 3698 (1 row) 
 INSERT INTO accounts(id, number, client, amount) VALUES (4, 3001, 'charlie', 100.00); 

Wir werden den Inhalt der Tabelle zusammen mit dem Feld cmin anzeigen (aber nur für die Zeilen, die durch unsere Transaktion hinzugefügt wurden - für andere ist dies nicht sinnvoll):

 => 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) 

Öffnen Sie nun den Cursor für die Abfrage, die die Anzahl der Zeilen in der Tabelle zurückgibt.

 => DECLARE c CURSOR FOR SELECT count(*) FROM accounts; 

Und danach noch eine Zeile hinzufügen:

 => INSERT INTO accounts(id, number, client, amount) VALUES (5, 3002, 'charlie', 200.00); 

Die Anforderung gibt 4 zurück - die nach dem Öffnen des Cursors hinzugefügte Zeile fällt nicht in den Datenschnappschuss:

 => FETCH c; 
  count ------- 4 (1 row) 

Warum? Denn im Snapshot werden nur Zeilenversionen 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 Nummer der frühesten aktiven Transaktion (snapshot.xmin) hat eine wichtige Bedeutung - sie definiert den "Ereignishorizont" der Transaktion. Über den Horizont hinaus sieht eine Transaktion nämlich immer nur aktuelle Versionen von Zeilen.

In der Tat muss eine irrelevante Version nur angezeigt werden, wenn die aktuelle Version durch eine Transaktion erstellt wurde, die noch nicht abgeschlossen wurde und daher noch nicht sichtbar ist. Über den "Horizont" hinaus ist jedoch bereits garantiert, dass alle Transaktionen abgeschlossen sind.



Der "Ereignishorizont" der Transaktion wird im Systemverzeichnis angezeigt:

 => BEGIN; => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid(); 
  backend_xmin -------------- 3699 (1 row) 

Sie können auch einen „Ereignishorizont“ auf Datenbankebene definieren. Machen Sie dazu alle aktiven Schnappschüsse und finden Sie unter ihnen die ältesten xmin. Es bestimmt den Horizont, ab dem irrelevante Versionen von Zeilen in dieser Datenbank für Transaktionen niemals sichtbar sind. Solche Versionen von Strings können gelöscht werden - weshalb das Konzept des Horizonts aus praktischer Sicht so wichtig ist.

Wenn eine Transaktion längere Zeit einen Snapshot enthält, enthält sie auch den Datenbankereignishorizont. Darüber hinaus wird eine unvollendete Transaktion aufgrund ihrer Existenz den Horizont bestimmen, selbst wenn kein Schnappschuss darin enthalten ist.

Dies bedeutet, dass irrelevante Versionen von Zeilen in dieser Datenbank nicht gelöscht werden können. Gleichzeitig darf sich eine „langwierige“ Transaktion nicht mit anderen Transaktionen in den Daten überschneiden - dies ist absolut nicht wichtig, der Datenbankhorizont ist für alle gleich.

Wenn jetzt nicht eine Transaktion, sondern Snapshots (von snapshot.xmin bis snapshot.xmax) als Segment dargestellt werden, kann die Situation wie folgt vorgestellt werden:



In dieser Abbildung bezieht sich der unterste Snapshot auf eine unvollständige Transaktion, und in den verbleibenden Snapshot.xmin-Snapshots darf die Anzahl nicht größer sein.

In unserem Beispiel wurde eine Transaktion mit der Isolationsstufe Read Committed gestartet. Obwohl es keinen aktiven Datenschnappschuss enthält, bleibt der Horizont erhalten:

 | => 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, sodass Sie irrelevante Versionen der Zeilen löschen können:

 => COMMIT; => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid(); 
  backend_xmin -------------- 3700 (1 row) 

Wenn die beschriebene Situation wirklich Probleme verursacht und es auf Anwendungsebene keine Möglichkeit gibt, sie zu vermeiden, stehen ab Version 9.6 zwei Optionen zur Verfügung:

  • old_snapshot_threshold definiert die maximale Lebensdauer eines Snapshots. Nach dieser Zeit hat der Server das Recht, irrelevante Versionen der Zeilen zu löschen. Wenn eine "langwierige" Transaktion erforderlich ist, wird jedoch ein zu alter Snapshot-Fehler angezeigt.
  • idle_in_transaction_session_timeout definiert die maximale Lebensdauer einer inaktiven Transaktion. Nach dieser Zeit wird die Transaktion abgebrochen.

Datenschnappschuss exportieren


Es gibt Situationen, in denen garantiert werden muss, dass mehrere Transaktionen gleichzeitig dasselbe Datenbild sehen. Als Beispiel können wir das Dienstprogramm pg_dump verwenden, das im parallelen Modus arbeiten kann: Alle Arbeitsprozesse müssen die Datenbank im selben Status sehen, damit die Sicherungskopie konsistent ist.

Natürlich können Sie sich nicht darauf verlassen, dass die Datenmuster einfach deshalb zusammenfallen, weil die Transaktionen „gleichzeitig“ gestartet werden. Hierfür gibt es einen Mechanismus zum Exportieren und Importieren eines Snapshots.

Die Funktion pg_export_snapshot gibt die Kennung eines Snapshots zurück, der (auf externe Weise an das DBMS) an eine andere Transaktion übertragen werden kann.

 => 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) 

Eine andere Transaktion kann den Snapshot mit dem Befehl SET TRANSACTION SNAPSHOT importieren, bevor die erste Anforderung darin ausgeführt wird. Sie müssen zuerst die Isolationsstufe als wiederholbares Lesen oder serialisierbar festlegen, da die Bediener auf der Ebene "Festgeschriebenes Lesen" ihre eigenen Snapshots verwenden.

 | => DELETE FROM accounts; | => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SET TRANSACTION SNAPSHOT '00000004-00000E7B-1'; 

Jetzt funktioniert die zweite Transaktion mit einem Schnappschuss der ersten und sieht dementsprechend drei Zeilen (und nicht Null):

 | => SELECT count(*) FROM accounts; 
 | count | ------- | 3 | (1 row) 

Die Lebensdauer des exportierten Snapshots entspricht der Lebensdauer der exportierenden Transaktion.

 | => COMMIT; => COMMIT; 

Fortsetzung folgt .

Source: https://habr.com/ru/post/de446652/


All Articles