Auf dieser Seite werden verschiedene Isolationsstufen 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 Isolationsebenen, die im ANSI/ISO-SQL-Standard definiert sind: serializable und repeatable read. Wenn Sie eine Transaktion erstellen, müssen Sie die am besten geeignete Isolationsstufe für die Transaktion auswählen. Mit der ausgewählten Isolationsstufe können einzelne Transaktionen verschiedene Faktoren wie Latenz, Abbruchrate und 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 serialisierbarer Isolation bietet Spanner die strengsten Garantien der Gleichzeitigkeitserkennung für Transaktionen. Dies wird als externe Konsistenz bezeichnet. Spanner verhält sich so, als ob alle Transaktionen sequenziell ausgeführt würden, obwohl sie tatsächlich über mehrere Server hinweg (womöglich sogar in mehreren Rechenzentren) ausgeführt werden, um höhere Leistung und Verfügbarkeit als bei Datenbanken mit 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 ist Spanner mit einer Datenbank auf einem einzelnen Rechner vergleichbar.
Der Nachteil ist, dass Spanner Transaktionen abbrechen kann, wenn eine Arbeitslast einen hohen Lese-/Schreibkonflikt aufweist. Das liegt an der grundlegenden Natur serialisierbarer Transaktionen, bei denen viele Transaktionen Daten lesen, die von einer anderen Transaktion aktualisiert werden. Dies ist jedoch eine gute Standardeinstellung für eine operative Datenbank. So lassen sich schwierige Timing-Probleme vermeiden, die normalerweise nur bei hoher Parallelität auftreten. Diese Probleme sind schwer zu reproduzieren und zu beheben. Daher bietet die serialisierbare Isolation den besten Schutz vor Datenanomalien. Wenn eine Transaktion wiederholt werden muss, kann es aufgrund von Transaktionswiederholungen zu einer erhöhten Latenz kommen.
Isolation durch wiederholbare Lesevorgänge
In Spanner wird die Isolation vom Typ „Repeatable Read“ mithilfe einer Technik implementiert, die allgemein als Snapshot-Isolation bezeichnet wird. Die Isolationsebene „Repeatable Read“ in Spanner sorgt dafür, dass bei allen Lesevorgängen innerhalb einer Transaktion ein konsistenter oder starker Snapshot der Datenbank angezeigt wird, wie er zu Beginn der Transaktion vorhanden war. Außerdem wird sichergestellt, dass gleichzeitige Schreibvorgänge für dieselben Daten nur erfolgreich sind, wenn keine Konflikte auftreten. Dieser Ansatz ist in Szenarien mit hohem Lese-/Schreibkonflikt von Vorteil, in denen zahlreiche Transaktionen Daten lesen, die von anderen Transaktionen geändert werden könnten. Durch die Verwendung eines festen Snapshots werden mit „Repeatable Read“ die Leistungseinbußen des restriktiveren serialisierbaren Isolationsgrads vermieden. Lesevorgänge können ohne Sperren und ohne gleichzeitige Schreibvorgänge zu blockieren ausgeführt werden. Dies führt zu weniger abgebrochenen Transaktionen, die aufgrund potenzieller Serialisierungskonflikte möglicherweise wiederholt werden müssen. In Anwendungsfällen, in denen Ihre Clients bereits alles in einer Lese-Schreib-Transaktion ausführen und es schwierig ist, schreibgeschützte Transaktionen neu zu entwerfen und zu verwenden, können Sie die Isolation „Repeatable Read“ verwenden, um die Latenz Ihrer Arbeitslasten zu verbessern.
Im Gegensatz zur serialisierbaren Isolation kann „Repeatable Read“ zu Datenanomalien führen, wenn Ihre Anwendung auf bestimmte Datenbeziehungen oder Einschränkungen angewiesen ist, die nicht durch das Datenbankschema erzwungen werden, insbesondere wenn die Reihenfolge der Vorgänge wichtig ist. In solchen Fällen kann es vorkommen, dass eine Transaktion Daten liest, Entscheidungen auf Grundlage dieser Daten trifft und dann Änderungen schreibt, 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 durch wiederholbare Lesevorgänge es ermöglicht, dass gleichzeitige Transaktionen ohne strikte Serialisierung ausgeführt werden. Eine mögliche Anomalie ist die Schreibabweichung, 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 zu jeder Zeit mindestens ein Arzt in Bereitschaft sein muss und Ärzte beantragen können, für eine Schicht aus der Bereitschaft genommen zu werden. Bei der Isolation „Wiederholbares Lesen“ werden beide Anfragen parallel ausgeführt, wenn sowohl Dr. Richards als auch Dr. Smith für dieselbe Schicht geplant sind und gleichzeitig versuchen, sich aus dem Bereitschaftsdienst abzumelden. Das liegt daran, dass bei beiden Transaktionen gelesen wird, dass zu Beginn der Transaktion mindestens ein weiterer Arzt im Bereitschaftsdienst ist. Wenn die Transaktionen erfolgreich sind, führt dies zu einer Datenanomalie. Wenn Sie jedoch die serialisierbare Isolation verwenden, wird verhindert, dass diese Transaktionen die Einschränkung verletzen, da serialisierbare Transaktionen potenzielle Datenanomalien erkennen und die Transaktion abbrechen. Dadurch wird die Anwendungsübereinstimmung sichergestellt, indem höhere Abbruchraten akzeptiert werden.
Im vorherigen Beispiel können Sie die SELECT FOR UPDATE
-Klausel in der Isolationsebene „Wiederholbares Lesen“ verwenden.
Mit der SELECT…FOR UPDATE
-Klausel wird geprüft, ob die Daten, die im ausgewählten Snapshot gelesen wurden, zum Commit-Zeitpunkt unverändert sind. Ähnlich verhält es sich mit DML-Anweisungen und Mutationen, die Daten intern lesen, um die Integrität der Schreibvorgänge zu gewährleisten. Sie prüfen auch, ob die Daten zum Zeitpunkt des Commits unverändert bleiben.
Weitere Informationen finden Sie unter Wiederholbare Leseisolation verwenden.
Anwendungsbeispiel
Das folgende Beispiel veranschaulicht den Vorteil der Verwendung der Isolationsebene „Repeatable Read“ zur Vermeidung von Sperren. Sowohl Transaction 1
als auch Transaction 2
werden mit der Isolationsebene „Wiederholbarer Lesevorgang“ ausgeführt.
Mit Transaction 1
wird ein Snapshot-Zeitstempel festgelegt, 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 |
*------------+------------------*/
Transaction 2
legt dann einen Snapshot-Zeitstempel fest, nachdem Transaction 1
beginnt, aber bevor er committet wird. Da Transaction 1
die Daten nicht aktualisiert hat, werden bei der SELECT
-Abfrage in Transaction 2
dieselben Daten wie bei Transaction 1
gelesen.
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
zugesichert 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 von Transaction 1
gelesenen Budgets. Diese Summe bezieht sich nur auf die Daten, die im T1
-Snapshot vorhanden sind. Das Budget, das Transaction 2
hinzugefügt hat, ist nicht enthalten, da Transaction 2
nach der Erstellung des Snapshots T1
am Transaction 1
zugesagt hat. Durch die Verwendung von „Repeatable Read“ musste Transaction 1
nicht abgebrochen werden, obwohl Transaction 2
die von Transaction 1
gelesenen Daten geändert hat. Das Ergebnis, das Spanner zurückgibt, entspricht jedoch möglicherweise nicht dem gewünschten Ergebnis.
Lese-/Schreibkonflikte und Richtigkeit
Wenn im vorherigen Beispiel die von den SELECT
-Anweisungen in Transaction 1
abgefragten Daten für nachfolgende Entscheidungen zum Marketingbudget verwendet wurden, kann es zu Problemen mit der Richtigkeit kommen.
Angenommen, es gibt ein Gesamtbudget von 400,000
. Anhand des Ergebnisses der SELECT
-Anweisung in Transaction 1
gehen wir möglicherweise davon aus, dass noch 100,000
im Budget verbleiben, und entscheiden uns, das gesamte Budget AlbumId = 4
zuzuweisen.
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
zugewiesen hat.
Mit der SELECT...FOR UPDATE
-Syntax können Sie prüfen, ob bestimmte Lesevorgänge einer Transaktion während der Lebensdauer der Transaktion unverändert bleiben, um die Richtigkeit der Transaktion zu gewährleisten. Im folgenden Beispiel mit SELECT...FOR UPDATE
wird Transaction 1
zur Commit-Zeit 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 in der Isolationsebene „Repeatable Read“ verwenden.
Schreib-Schreib-Konflikte und Richtigkeit
Bei Verwendung der Isolationsebene „Wiederholbares Lesen“ können gleichzeitige Schreibvorgänge für dieselben Daten nur erfolgreich ausgeführt werden, wenn keine Konflikte auftreten.
Im folgenden Beispiel wird mit Transaction 1
ein Snapshot-Zeitstempel bei der ersten SELECT
-Anweisung festgelegt.
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;
Mit dem folgenden Transaction 2
werden dieselben Daten wie mit Transaction 1
gelesen und ein neues Element eingefügt. Transaction 2
wird erfolgreich übernommen, 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
zugesichert 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
Informationen zur Verwendung der Isolationsstufe „Wiederholbares Lesen“
Informationen zur Verwendung von SELECT FOR UPDATE in der Isolationsebene „Repeatable Read“
Informationen zur Verwendung von SELECT FOR UPDATE in der serialisierbaren Isolation
Weitere Informationen zur Serialisierbarkeit und externen Konsistenz von Spanner finden Sie unter TrueTime und externe Konsistenz.