Ü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 für die Transaktion am besten geeignete Isolationsebene auswählen. Mit der ausgewählten Isolationsebene können einzelne Transaktionen verschiedene Faktoren wie Latenz, Abbruchrate und die Anfälligkeit der Anwendung für die Auswirkungen von Datenanomalien priorisieren. 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 einzelnen Servern zu erzielen. Wenn eine Transaktion abgeschlossen wird, bevor eine andere Transaktion mit dem Commit 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 Nebenläufigkeit von Lese-/Schreibvorgängen 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 Nebenläufigkeit von Lese-/Schreibvorgängen 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. Lesevorgänge können ausgeführt werden, ohne Sperren zu erwerben und ohne gleichzeitige Schreibvorgänge zu blockieren. Das führt zu weniger abgebrochenen Transaktionen, die aufgrund potenzieller Serialisierungskonflikte wiederholt werden müssen. In Anwendungsfällen, in denen Ihre Clients bereits alles in einer Lese-/Schreibtransaktion ausführen und es schwierig ist, schreibgeschützte Transaktionen neu zu entwerfen und zu verwenden, können Sie die Isolation wiederholbarer Lesevorgänge verwenden, um die Latenz Ihrer Arbeitslasten zu verbessern.

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 der 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 wird hingegen 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 sichergestellt, 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 sicherzustellen, ob die Daten zum Zeitpunkt des Commits unverändert sind.

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 Anweisung SELECT 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 ein Commit durchgeführt wird. Da Transaction 1 die Daten nicht aktualisiert hat, liest die Abfrage SELECT 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 einen Commit durchgeführt hat.

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 einen Commit durchgeführt hat, 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 im vorherigen Beispiel die von den Anweisungen SELECT in Transaction 1 abgefragten Daten verwendet wurden, um nachfolgende Entscheidungen zum Marketingbudget zu treffen, kann es 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 durchgeführt, 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.

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 bei der ersten Anweisung SELECT einen Snapshot-Zeitstempel 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;

Transaction 2 liest dieselben Daten wie Transaction 1 und fügt ein neues Element ein. Transaction 2 führt erfolgreich einen Commit durch, 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 einen Commit durchgeführt hat.

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 durchgeführt hat.

Nächste Schritte