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 der serialisierbaren Isolation bietet Spanner Ihnen die strengsten Garantien bezüglich der Nebenläufigkeitserkennung für Transaktionen. Dies wird als externe Konsistenz bezeichnet. Cloud Spanner verhält sich so, als ob alle Transaktionen sequenziell ausgeführt würden, obwohl sie tatsächlich auf mehreren Servern (und möglicherweise in mehreren Rechenzentren) ausgeführt werden, 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 Cloud Spanner einer Datenbank auf einem einzelnen Computer ähnlich.
Der Nachteil ist, dass Spanner Transaktionen abbrechen kann, wenn ein Arbeitslast mit hoher Nebenläufigkeit von Lese-/Schreibvorgängen vorliegt, bei der viele Transaktionen Daten lesen, die von einer anderen Transaktion aktualisiert werden. Dies liegt an der grundlegenden Natur serialisierbarer Transaktionen. Dies ist jedoch eine gute Standardeinstellung für eine operative Datenbank. So lassen sich schwierige Timing-Probleme vermeiden, die normalerweise nur bei hoher Nebenläufigkeit auftreten. Diese Probleme sind schwer zu reproduzieren und zu beheben. Die serialisierbare Isolation bietet daher 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 wiederholbarer Lesevorgänge mithilfe 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 sichergestellt, 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 werden mit „Repeatable Read“ die Leistungseinbußen des restriktiveren Serialisierbarkeits-Isolationslevels vermieden.
Bei der standardmäßigen optimistischen Parallelität werden Lesevorgänge ausgeführt, ohne dass Sperren abgerufen werden und ohne dass gleichzeitige Schreibvorgänge blockiert werden. Dies führt zu weniger abgebrochenen Transaktionen, die aufgrund potenzieller Serialisierungskonflikte möglicherweise wiederholt werden müssen. Bei pessimistischer Parallelität werden für Lesevorgänge Snapshots verwendet, aber exklusive Sperren werden auf Daten angewendet, die mit FOR UPDATE-Abfragen oder lock_scanned_ranges=exclusive-Hinweisen gelesen werden, und auf Daten, die mit DML-Abfragen geschrieben werden.
Für Arbeitslasten, die von anderen Datenbanken migriert werden, empfehlen wir, Ihre Anwendung für die Verwendung der Isolation „Wiederholbares Lesen“ in Spanner zu konfigurieren. Die Semantik für Transaktionen mit wiederholbarem Lesen, insbesondere die Sperrung für Lesevorgänge, entspricht den Standardisolationsstufen in den meisten anderen Datenbanken (z. B. MySQL und PostgreSQL). So müssen Sie Ihre Anwendung nicht neu entwerfen, damit sie mit der standardmäßigen serialisierbaren Isolationsebene von Spanner funktioniert.
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 bei der Isolation wiederholbarer Lesevorgänge gleichzeitige Transaktionen ohne strikte Serialisierung ausgeführt werden können. 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 Krankenhaus vor, in dem zu jeder Zeit mindestens ein Arzt Bereitschaftsdienst haben muss und Ärzte beantragen können, für eine Schicht vom Bereitschaftsdienst befreit 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 gegen die Einschränkung verstoßen, 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. Bei pessimistischer Nebenläufigkeit werden für die von SELECT ... FOR UPDATE gelesenen Daten und die von DML-Anweisungen geschriebenen Daten außerdem exklusive Sperren abgerufen, um zu verhindern, dass zukünftige Transaktionen widersprüchliche Änderungen vor dem Commit der aktuellen Transaktion committen.
Für Arbeitslasten, die von anderen Datenbanken migriert werden, in denen FOR UPDATE-Abfragen verwendet werden, empfehlen wir, Ihre Anwendung so zu konfigurieren, dass sie in Spanner die Isolation vom Typ „Wiederholbares Lesen“ mit pessimistischer Parallelität verwendet. Die Anwendung ruft weiterhin Sperren für die von SELECT ... FOR UPDATE gelesenen Daten ab. Das ist das Standardverhalten in anderen Datenbanken.
Weitere Informationen finden Sie unter Isolation wiederholbarer Lesevorgänge 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 „Repeatable Read“ (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 Transaction 1 T1 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 könnten wir annehmen, dass noch 100,000 im Budget übrig ist, und uns entscheiden, das gesamte Budget 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 übertragen, obwohl Transaction 2 bereits 50,000 des verbleibenden 100,000-Budgets 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 Isolation wiederholbarer Lesevorgänge verwenden.
Sie können auch die pessimistische Parallelität verwenden, bei der exklusive Sperren für Daten übernommen werden, die von der SELECT...FOR UPDATE-Anweisung gelesen werden. Beispiel: Transaction 1 wird zur Commit-Zeit abgebrochen, weil Transaction 2 seine Änderungen vor dem Erwerb von Sperren durch Transaction 1 committet 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 erworben hat, wartet Transaction 2 darauf, dass Transaction 1 die Sperren festschreibt und freigibt, bevor es fortfahren kann. Bei der pessimistischen Nebenläufigkeitsoption wird der Zugriff auf Daten serialisiert.
Weitere Informationen finden Sie unter Nebenläufigkeitserkennung.
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 ausgeführt, 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 vorgenommen hat.
Nächste Schritte
Informationen zur Verwendung der Isolationsstufe „Wiederholbares Lesen“
Informationen zur Verwendung von SELECT FOR UPDATE bei der Isolation wiederholbarer Lesevorgänge
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.