Übersicht über die Isolationsebenen

Auf dieser Seite werden verschiedene Isolationsebenen vorgestellt und ihre Funktionsweise in Spanner erläutert.

Die Isolationsebene ist eine Datenbankeigenschaft, die definiert, welche Daten für gleichzeitige Transaktionen sichtbar sind. Spanner unterstützt zwei der im ANSI/ISO-SQL-Standard definierten Isolationsebenen: serialisierbar und wiederholbarer Lesevorgang. Wenn Sie eine Transaktion erstellen, müssen Sie die am besten geeignete Isolationsebene für die Transaktion auswählen. Mit der ausgewählten Isolationsebene können einzelne Transaktionen verschiedene Faktoren priorisieren, z. B. Latenz, Abbruchrate und ob die Anwendung anfällig für die Auswirkungen von Datenanomalien ist. Die beste Wahl hängt von den spezifischen Anforderungen der Arbeitslast ab.

Serialisierbare Isolation

Die serialisierbare Isolation ist die Standardisolationsebene in Spanner. Bei der serialisierbaren Isolation bietet Spanner die strengsten Garantien bezüglich der Nebenläufigkeitserkennung für Transaktionen. Dies wird als externe Konsistenz bezeichnet. Spanner verhält sich so, als ob alle Transaktionen sequenziell ausgeführt würden, obwohl Spanner sie tatsächlich auf mehreren Servern (und möglicherweise in mehreren Rechenzentren) ausführt, um eine höhere Leistung und Verfügbarkeit als bei Datenbanken auf einem einzelnen Server zu erzielen. Wenn eine Transaktion abgeschlossen ist, bevor der Commit einer anderen Transaktion beginnt, garantiert Spanner außerdem, dass Clients die Ergebnisse von Transaktionen immer in sequenzieller Reihenfolge sehen. Intuitiv betrachtet ist Spanner einer Datenbank auf einem einzelnen Computer ähnlich.

Der Nachteil ist, dass Spanner Transaktionen möglicherweise abbricht, wenn eine Arbeitslast eine hohe Lese-/Schreibkonkurrenz aufweist, bei der viele Transaktionen Daten lesen, die von einer anderen Transaktion aktualisiert werden. Das liegt an der grundlegenden Natur serialisierbarer Transaktionen. Dies ist jedoch eine gute Standardeinstellung für eine operative Datenbank. So lassen sich schwierige Timingprobleme vermeiden, die normalerweise nur bei hoher Nebenläufigkeit auftreten. Diese Probleme sind schwer zu reproduzieren und zu beheben. Daher bietet die serialisierbare Isolation den stärksten Schutz vor Datenanomalien. Wenn eine Transaktion wiederholt werden muss, kann es aufgrund von Transaktionswiederholungen zu einer erhöhten Latenz kommen.

Isolation wiederholbarer Lesevorgänge

In Spanner wird die Isolation wiederholbarer Lesevorgänge mit einer Technik implementiert, die allgemein als Snapshot-Isolation bezeichnet wird. Die Isolation wiederholbarer Lesevorgänge in Spanner sorgt dafür, dass alle Lesevorgänge innerhalb einer Transaktion einen konsistenten oder starken Snapshot der Datenbank sehen, wie er zu Beginn der Transaktion vorlag. Außerdem wird garantiert, dass gleichzeitige Schreibvorgänge für dieselben Daten nur erfolgreich sind, wenn keine Konflikte auftreten. Dieser Ansatz ist in Szenarien mit hoher Lese-/Schreibkonkurrenz vorteilhaft, in denen zahlreiche Transaktionen Daten lesen, die von anderen Transaktionen geändert werden könnten. Durch die Verwendung eines festen Snapshots vermeidet die Isolation wiederholbarer Lesevorgänge die Leistungseinbußen der restriktiveren serialisierbaren Isolationsebene.

Bei der standardmäßigen optimistischen Nebenläufigkeit, werden Lesevorgänge ohne Sperren und ohne Blockieren gleichzeitiger Schreibvorgänge ausgeführt. Das führt zu weniger abgebrochenen Transaktionen, die aufgrund potenzieller Serialisierungskonflikte wiederholt werden müssen. Bei pessimistischer Nebenläufigkeit, verwenden Lesevorgänge Snapshots, aber exklusive Sperren gelten für Daten, die aus FOR UPDATE Abfragen oder lock_scanned_ranges=exclusive Hinweisen gelesen wurden, und für Daten, die mit DML-Abfragen geschrieben wurden.

Für Arbeitslasten, die von anderen Datenbanken migriert werden, empfehlen wir, die Anwendung so zu konfigurieren, dass die Isolation wiederholbarer Lesevorgänge in Spanner verwendet wird. Die Semantik von Transaktionen mit wiederholbaren Lesevorgängen, insbesondere die Sperrung für Lesevorgänge, entspricht den Standardisolationsebenen in den meisten anderen Datenbanken (z. B. MySQL und PostgreSQL). So müssen Sie Ihre Anwendung nicht neu entwerfen, um sie mit der standardmäßigen serialisierbaren Isolationsebene von Spanner zu verwenden.

Im Gegensatz zur serialisierbaren Isolation kann die Isolation wiederholbarer Lesevorgänge zu Datenanomalien führen, wenn Ihre Anwendung auf bestimmten Datenbeziehungen oder Einschränkungen basiert, die nicht durch das Datenbankschema erzwungen werden, insbesondere wenn die Reihenfolge der Vorgänge wichtig ist. In solchen Fällen kann eine Transaktion Daten lesen, Entscheidungen auf Grundlage dieser Daten treffen und dann Änderungen schreiben, die gegen diese anwendungsspezifischen Einschränkungen verstoßen, auch wenn die Einschränkungen des Datenbankschemas weiterhin erfüllt sind. Das liegt daran, dass die Isolation wiederholbarer Lesevorgänge gleichzeitige Transaktionen ohne strenge Serialisierung zulässt. Eine mögliche Anomalie ist die sogenannte Schreibverzerrung, die durch eine bestimmte Art von gleichzeitiger Aktualisierung entsteht. Dabei wird jede Aktualisierung unabhängig akzeptiert, aber ihre kombinierte Wirkung verstößt gegen die Datenintegrität der Anwendung. Stellen Sie sich beispielsweise ein Krankenhaussystem vor, in dem immer mindestens ein Arzt im Bereitschaftsdienst sein muss und Ärzte beantragen können, für eine Schicht vom Bereitschaftsdienst befreit zu werden. Bei der Isolation wiederholbarer Lesevorgänge werden beide Anfragen parallel ausgeführt, wenn sowohl Dr. Richards als auch Dr. Smith für dieselbe Schicht im Bereitschaftsdienst sind und gleichzeitig beantragen, vom Bereitschaftsdienst befreit zu werden. Das liegt daran, dass beide Transaktionen lesen, dass zu Beginn der Transaktion mindestens ein anderer Arzt im Bereitschaftsdienst ist. Das führt zu einer Datenanomalie, wenn die Transaktionen erfolgreich sind. Bei der serialisierbaren Isolation hingegen wird verhindert, dass diese Transaktionen gegen die Einschränkung verstoßen, da serialisierbare Transaktionen potenzielle Datenanomalien erkennen und die Transaktion abbrechen. So wird die Konsistenz der Anwendung gewährleistet, indem höhere Abbruchraten akzeptiert werden.

Im vorherigen Beispiel können Sie die Klausel SELECT FOR UPDATE bei der Isolation wiederholbarer Lesevorgänge verwenden. Die Klausel SELECT ... FOR UPDATE prüft, ob die Daten, die im ausgewählten Snapshot gelesen wurden, zum Zeitpunkt des Commits unverändert sind. Ähnlich prüfen auch DML-Anweisungen und Mutationen, die Daten intern lesen, um die Integrität der Schreibvorgänge zu gewährleisten, ob die Daten zum Zeitpunkt des Commits unverändert sind. Zusätzlich erhalten bei pessimistischer Nebenläufigkeit die Daten, die von SELECT ... FOR UPDATE gelesen wurden, und die Daten, die von DML-Anweisungen geschrieben wurden, außerdem exklusive Sperren, um zu verhindern, dass zukünftige Transaktionen widersprüchliche Änderungen committen, bevor die aktuelle Transaktion committet wird.

Für Arbeitslasten, die von anderen Datenbanken migriert werden, die FOR UPDATE-Abfragen verwenden, empfehlen wir, die Anwendung so zu konfigurieren, dass die Isolation wiederholbarer Lesevorgänge mit pessimistischer Nebenläufigkeit in Spanner verwendet wird. Die Anwendung ruft weiterhin Sperren für die Daten ab, die von SELECT ... FOR UPDATE gelesen wurden. Das ist das Standardverhalten in anderen Datenbanken.

Weitere Informationen finden Sie unter Isolation wiederholbarer Lesevorgänge verwenden.

Anwendungsbeispiel

Das folgende Beispiel zeigt den Vorteil der Verwendung der Isolation wiederholbarer Lesevorgänge, um den Sperraufwand zu reduzieren. Sowohl Transaction 1 als auch Transaction 2 werden mit der Isolation wiederholbarer Lesevorgänge ausgeführt.

Transaction 1 legt einen Snapshot-Zeitstempel fest, wenn die SELECT-Anweisung ausgeführt wird.

GoogleSQL

-- Transaction 1
BEGIN;

-- Snapshot established at T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;

/*-----------+------------------*
| AlbumId    | MarketingBudget  |
+------------+------------------+
| 1          | 50000            |
| 2          | 100000           |
| 3          | 70000            |
| 4          | 80000            |
*------------+------------------*/

PostgreSQL

-- Transaction 1
BEGIN;

-- Snapshot established at T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;

/*-----------+------------------*
| albumid    | marketingbudget  |
+------------+------------------+
| 1          | 50000            |
| 2          | 100000           |
| 3          | 70000            |
| 4          | 80000            |
*------------+------------------*/

Dann legt Transaction 2 einen Snapshot-Zeitstempel fest, nachdem Transaction 1 begonnen hat, aber bevor sie committet wird. Da Transaction 1 die Daten nicht aktualisiert hat, liest die SELECT-Abfrage in Transaction 2 dieselben Daten wie Transaction 1.

GoogleSQL

-- Transaction 2
BEGIN;

-- Snapshot established at T2 > T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;

INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 50000);

COMMIT;

PostgreSQL

-- Transaction 2
BEGIN;

-- Snapshot established at T2 > T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;

INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 50000);

COMMIT;

Transaction 1 wird fortgesetzt, nachdem Transaction 2 committet wurde.

GoogleSQL

-- Transaction 1 continues
SELECT SUM(MarketingBudget) as UsedBudget
FROM Albums
WHERE SingerId = 1;

/*-----------*
| UsedBudget |
+------------+
| 300000     |
*------------*/

PostgreSQL

-- Transaction 1 continues
SELECT SUM(marketingbudget) AS usedbudget
FROM albums
WHERE singerid = 1;

/*-----------*
| usedbudget |
+------------+
| 300000     |
*------------*/

Der von Spanner zurückgegebene Wert UsedBudget ist die Summe des Budgets, das von Transaction 1 gelesen wurde. Diese Summe spiegelt nur die Daten wider, die im Snapshot T1 vorhanden sind. Das von Transaction 2 hinzugefügte Budget ist nicht enthalten, da Transaction 2 committet wurde, nachdem Transaction 1 den Snapshot T1 erstellt hat. Durch die Verwendung der Isolation wiederholbarer Lesevorgänge musste Transaction 1 nicht abgebrochen werden, obwohl Transaction 2 die von Transaction 1 gelesenen Daten geändert hat. Das von Spanner zurückgegebene Ergebnis ist jedoch möglicherweise nicht das gewünschte Ergebnis.

Lese-/Schreibkonflikte und Korrektheit

Wenn die Daten, die von den SELECT-Anweisungen in Transaction 1 abgefragt wurden, für nachfolgende Entscheidungen zum Marketingbudget verwendet werden, kann es im vorherigen Beispiel zu Problemen mit der Korrektheit kommen.

Angenommen, es gibt ein Gesamtbudget von 400,000. Basierend auf dem Ergebnis der SELECT-Anweisung in Transaction 1 könnten wir annehmen, dass noch 100,000 im Budget übrig sind, und beschließen, alles für AlbumId = 4 zu verwenden.

GoogleSQL

-- Transaction 1 continues..
UPDATE Albums
SET MarketingBudget = MarketingBudget + 100000
WHERE SingerId = 1 AND AlbumId = 4;

COMMIT;

PostgreSQL

-- Transaction 1 continues..
UPDATE albums
SET marketingbudget = marketingbudget + 100000
WHERE singerid = 1 AND albumid = 4;

COMMIT;

Transaction 1 wird erfolgreich committet, obwohl Transaction 2 bereits 50,000 des verbleibenden Budgets von 100,000 für ein neues Album AlbumId = 5 verwendet hat.

Mit der Syntax SELECT...FOR UPDATE können Sie prüfen, ob bestimmte Lesevorgänge einer Transaktion während der Lebensdauer der Transaktion unverändert sind, um die Korrektheit der Transaktion zu garantieren. Im folgenden Beispiel mit SELECT...FOR UPDATE wird Transaction 1 zum Zeitpunkt des Commits abgebrochen.

GoogleSQL

-- Transaction 1 continues..
SELECT SUM(MarketingBudget) AS TotalBudget
FROM Albums
WHERE SingerId = 1
FOR UPDATE;

/*-----------*
| TotalBudget |
+------------+
| 300000     |
*------------*/

COMMIT;

PostgreSQL

-- Transaction 1 continues..
SELECT SUM(marketingbudget) AS totalbudget
FROM albums
WHERE singerid = 1
FOR UPDATE;

/*-------------*
 | totalbudget |
 +-------------+
 | 300000      |
 *-------------*/

COMMIT;

Weitere Informationen finden Sie unter SELECT FOR UPDATE bei der Isolation wiederholbarer Lesevorgänge verwenden.

Sie können auch die pessimistische Nebenläufigkeit verwenden, die exklusive Sperren für Daten abruft, die von der Anweisung SELECT...FOR UPDATE gelesen wurden. Beispielsweise wird Transaction 1 zum Zeitpunkt des Commits abgebrochen, weil Transaction 2 die Änderungen committet hat, bevor Transaction 1 Sperren abgerufen hat. Das führt zu einem Konflikt. Wenn die Transaktionsreihenfolge jedoch dazu führt, dass Transaction 2 versucht, das Marketingbudget zu aktualisieren, nachdem Transaction 1 Sperren abgerufen hat, wartet Transaction 2, bis Transaction 1 committet wurde und die Sperren freigegeben hat, bevor sie fortgesetzt werden kann. Die Option für pessimistische Nebenläufigkeit serialisiert den Zugriff auf Daten.

Weitere Informationen finden Sie unter Nebenläufigkeitserkennung.

Schreib-/Schreibkonflikte und Korrektheit

Bei der Isolation wiederholbarer Lesevorgänge sind gleichzeitige Schreibvorgänge für dieselben Daten nur erfolgreich, wenn keine Konflikte auftreten.

Im folgenden Beispiel legt Transaction 1 einen Snapshot-Zeitstempel bei der ersten SELECT-Anweisung fest.

GoogleSQL

-- Transaction 1
BEGIN;

-- Snapshot established at T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;

PostgreSQL

-- Transaction 1
BEGIN;

-- Snapshot established at T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;

Die folgende Transaction 2 liest dieselben Daten wie Transaction 1 und fügt ein neues Element ein. Transaction 2 wird erfolgreich committet, ohne zu warten oder abzubrechen.

GoogleSQL

-- Transaction 2
BEGIN;

-- Snapshot established at T2 (> T1)
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;

INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 50000);

COMMIT;

PostgreSQL

-- Transaction 2
BEGIN;

-- Snapshot established at T2 (> T1)
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;

INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 50000);

COMMIT;

Transaction 1 wird fortgesetzt, nachdem Transaction 2 committet wurde.

GoogleSQL

-- Transaction 1 continues
INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 30000);
-- Transaction aborts
COMMIT;

PostgreSQL

-- Transaction 1 continues
INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 30000);
-- Transaction aborts
COMMIT;

Transaction 1 wird abgebrochen, da Transaction 2 bereits eine Einfügung in die Zeile AlbumId = 5 committet hat.

Nächste Schritte