Database
 sql >> Datenbank >  >> RDS >> Database

Datenänderungen unter Read Committed Snapshot Isolation

[Siehe den Index für die gesamte Serie]

Der vorherige Beitrag in dieser Reihe zeigte, wie eine T-SQL-Anweisung, die unter Snapshot-Isolation mit Lesebestätigung (RCSI ) sieht normalerweise eine Momentaufnahme des festgeschriebenen Zustands der Datenbank, wie er war, als die Ausführung der Anweisung gestartet wurde. Das ist eine gute Beschreibung der Funktionsweise von Anweisungen, die Daten lesen, aber es gibt wichtige Unterschiede für Anweisungen, die unter RCSI ausgeführt werden und vorhandene Zeilen ändern .

Ich betone die Änderung bestehender Zeilen oben, da die folgenden Überlegungen nur für UPDATE gelten und DELETE Operationen (und die entsprechenden Aktionen eines MERGE Erklärung). Um es klar zu sagen, INSERT Aussagen sind ausdrücklich ausgeschlossen von dem Verhalten, das ich gleich beschreiben werde, da Einfügungen vorhandene nicht ändern Daten.

Sperren und Zeilenversionen aktualisieren

Der erste Unterschied besteht darin, dass update- und delete-Anweisungen keine Zeilenversionen lesen unter RCSI, wenn Sie nach den zu ändernden Quellzeilen suchen. Update- und Delete-Anweisungen unter RCSI erwerben stattdessen Update-Sperren bei der Suche nach qualifizierten Zeilen. Durch die Verwendung von Aktualisierungssperren wird sichergestellt, dass der Suchvorgang mit den neuesten festgeschriebenen Daten zu ändernde Zeilen findet .

Ohne Aktualisierungssperre würde die Suche auf einer möglicherweise veralteten Version des Datensatzes basieren (festgeschriebene Daten, wie sie zum Zeitpunkt des Starts der Datenänderungsanweisung waren). Dies erinnert Sie vielleicht an das Trigger-Beispiel, das wir letztes Mal gesehen haben, wo ein READCOMMITTEDLOCK Hint wurde verwendet, um von RCSI zur Sperrimplementierung der Read-Committed-Isolation zurückzukehren. Dieser Hinweis war in diesem Beispiel erforderlich, um zu vermeiden, dass eine wichtige Aktion auf veralteten Informationen basiert. Die gleiche Art von Argumentation wird hier verwendet. Ein Unterschied besteht darin, dass die READCOMMITTEDLOCK hint erwirbt gemeinsam genutzte Sperren anstelle von Update-Sperren. Darüber hinaus erwirbt SQL Server automatisch Aktualisierungssperren, um Datenänderungen unter RCSI zu schützen, ohne dass wir einen expliziten Hinweis hinzufügen müssen.

Das Setzen von Update-Sperren stellt auch sicher, dass die Update- oder Delete-Anweisung blockiert wird wenn es auf eine inkompatible Sperre stößt, zum Beispiel eine exklusive Sperre, die eine In-Flight-Datenänderung schützt, die von einer anderen gleichzeitigen Transaktion durchgeführt wird.

Eine zusätzliche Komplikation besteht darin, dass das geänderte Verhalten nur gilt zu der Tabelle, die das Ziel ist des Aktualisierungs- oder Löschvorgangs. Andere Tabellen im gleichen Anweisung zum Löschen oder Aktualisieren, einschließlich zusätzlicher Verweise zur Zieltabelle, verwenden Sie weiterhin Zeilenversionen .

Einige Beispiele sind wahrscheinlich erforderlich, um diese verwirrenden Verhaltensweisen etwas klarer zu machen…

Setup testen

Das folgende Skript stellt sicher, dass wir alle für die Verwendung von RCSI eingerichtet sind, erstellt eine einfache Tabelle und fügt ihr zwei Beispielzeilen hinzu:

ALTER DATABASE Sandpit
SET READ_COMMITTED_SNAPSHOT ON
WITH ROLLBACK IMMEDIATE;
GO
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
GO
CREATE TABLE dbo.Test
(
    RowID integer PRIMARY KEY,
    Data integer NOT NULL
);
GO
INSERT dbo.Test
    (RowID, Data)
VALUES 
    (1, 1234),
    (2, 2345);

Der nächste Schritt muss in einer separaten Sitzung ausgeführt werden . Es startet eine Transaktion und löscht beide Zeilen aus der Testtabelle (scheint seltsam, aber das wird in Kürze alles Sinn machen):

BEGIN TRANSACTION;
DELETE dbo.Test 
WHERE RowID IN (1, 2);

Beachten Sie, dass die Transaktion absichtlich offen gelassen wird . Dadurch werden exklusive Sperren für beide zu löschenden Zeilen aufrechterhalten (zusammen mit den üblichen beabsichtigten exklusiven Sperren auf der enthaltenden Seite und der Tabelle selbst), wie die folgende Abfrage zeigen kann:

SELECT
    resource_type,
    resource_description,
    resource_associated_entity_id,
    request_mode,
    request_status
FROM sys.dm_tran_locks
WHERE 
    request_session_id = @@SPID;

Der Auswahltest

Zurückschalten zur ursprünglichen Sitzung , das erste, was ich zeigen möchte, ist, dass bei regulären select-Anweisungen, die RCSI verwenden, die beiden Zeilen weiterhin gelöscht werden. Die nachstehende Auswahlabfrage verwendet Zeilenversionen, um die letzten festgeschriebenen Daten zum Zeitpunkt des Beginns der Anweisung zurückzugeben:

SELECT *
FROM dbo.Test;

Falls dies überraschend erscheint, denken Sie daran, dass das Anzeigen der Zeilen als gelöscht bedeuten würde, dass eine nicht festgeschriebene Ansicht der Daten angezeigt wird, was bei der Read-Commited-Isolation nicht zulässig ist.

Der Löschtest

Trotz erfolgreichem Auswahltest ein Löschversuch dieselben Zeilen aus der aktuellen Sitzung werden blockiert. Sie können sich vorstellen, dass diese Blockierung auftritt, wenn die Operation versucht, exclusive zu erwerben Sperren, aber das ist nicht der Fall.

Beim Löschen wird keine Zeilenversionierung verwendet um die zu löschenden Zeilen zu finden; es versucht stattdessen, Update-Sperren zu erwerben. Aktualisierungssperren sind nicht mit den exklusiven Zeilensperren kompatibel, die von der Sitzung mit der offenen Transaktion gehalten werden, daher blockiert die Abfrage:

DELETE dbo.Test 
WHERE RowID IN (1, 2);

Der geschätzte Abfrageplan für diese Anweisung zeigt, dass die zu löschenden Zeilen durch eine reguläre Suchoperation identifiziert werden, bevor ein separater Operator die eigentliche Löschung durchführt:

Wir können die in diesem Stadium gehaltenen Sperren sehen, indem wir dieselbe Sperrabfrage wie zuvor (von einer anderen Sitzung) ausführen und dabei daran denken, die SPID-Referenz in die von der blockierten Abfrage verwendete zu ändern. Die Ergebnisse sehen so aus:

Unsere Löschabfrage wird beim Clustered Index Seek-Operator blockiert, der darauf wartet, eine Aktualisierungssperre zum Lesen zu erwerben Daten. Dies zeigt, dass das Auffinden der zu löschenden Zeilen unter RCSI Aktualisierungssperren erwirbt, anstatt potenziell veraltete versionierte Daten zu lesen. Es zeigt auch, dass die Blockierung nicht darauf zurückzuführen ist, dass der Löschteil der Operation darauf wartet, eine exklusive Sperre zu erwerben.

Der Aktualisierungstest

Brechen Sie die blockierte Abfrage ab und versuchen Sie stattdessen das folgende Update:

UPDATE dbo.Test
SET Data = Data + 1000
WHERE RowID IN (1, 2);

Der geschätzte Ausführungsplan ähnelt dem im Löschtest:

Der Berechnungsskalar dient dazu, das Ergebnis der Addition von 1000 zum aktuellen Wert der Datenspalte in jeder Zeile zu bestimmen, die von der Clustered-Index-Suche gelesen wird. Diese Anweisung wird auch blockieren bei der Ausführung aufgrund der von der Leseoperation angeforderten Aktualisierungssperre. Der folgende Screenshot zeigt die Sperren, die gehalten werden, wenn die Abfrage blockiert:

Wie zuvor wird die Abfrage beim Suchen blockiert und wartet darauf, dass die inkompatible exklusive Sperre freigegeben wird, damit eine Aktualisierungssperre erworben werden kann.

Der Insert-Test

Der nächste Test enthält eine Anweisung, die eine neue Zeile in unsere Testtabelle einfügt, wobei der Datenspaltenwert aus der vorhandenen Zeile verwendet wird mit ID 1 in der Tabelle. Denken Sie daran, dass diese Zeile immer noch ausschließlich durch die Sitzung mit der offenen Transaktion gesperrt ist:

INSERT dbo.Test
    (RowID, Data)
SELECT 3, Data
FROM dbo.Test
WHERE RowID = 1;

Der Ausführungsplan ähnelt wieder den vorherigen Tests:

Diesmal wird die Abfrage nicht blockiert . Dies zeigt, dass beim Lesen keine Update-Sperren erworben wurden Daten für die Einlage. Diese Abfrage verwendete stattdessen die Zeilenversionsverwaltung, um den Datenspaltenwert für die neu eingefügte Zeile abzurufen. Es wurden keine Aktualisierungssperren erworben, weil diese Anweisung keine Zeilen zum Ändern gefunden hat , es wurden lediglich Daten gelesen, die in der Einfügung verwendet werden sollen.

Wir können diese neue Zeile in der Tabelle mit der Auswahltestabfrage von zuvor sehen:

Beachten Sie, dass wir sind in der Lage, die neue Zeile zu aktualisieren und zu löschen (wofür Aktualisierungssperren erforderlich sind), da es keine widersprüchliche exklusive Sperre gibt. Die Sitzung mit der offenen Transaktion hat nur exklusive Sperren auf den Zeilen 1 und 2:

-- Update the new row
UPDATE dbo.Test
SET Data = 9999
WHERE RowID = 3;
-- Show the data
SELECT * FROM dbo.Test;
-- Delete the new row
DELETE dbo.Test
WHERE RowID = 3;

Dieser Test bestätigt, dass insert-Anweisungen beim Lesen keine Aktualisierungssperren erwerben , da sie im Gegensatz zu Aktualisierungen und Löschungen nicht modifizieren eine bestehende Reihe. Der Leseteil einer Einfügung -Anweisung verwendet das normale RCSI-Zeilenversionierungsverhalten.

Mehrere Referenztests

Ich habe bereits erwähnt, dass nur die einzelne Tabellenreferenz, die zum Auffinden von zu ändernden Zeilen verwendet wird, Aktualisierungssperren erwirbt; andere Tabellen in derselben update- oder delete-Anweisung lesen weiterhin Zeilenversionen. Als Sonderfall dieses allgemeinen Prinzips eine Datenänderungsanweisung mit mehreren Verweisen auf dieselbe Tabelle wendet Update-Sperren nur auf die eine Instanz an Wird verwendet, um zu ändernde Zeilen zu finden. Dieser abschließende Test veranschaulicht dieses komplexere Verhalten Schritt für Schritt.

Das erste, was wir brauchen, ist eine neue dritte Zeile für unsere Testtabelle, diesmal mit einer Null in der Datenspalte:

INSERT dbo.Test
    (RowID, Data)
VALUES
    (3, 0);

Wie erwartet wird diese Einfügung ohne Blockierung fortgesetzt, was zu einer Tabelle führt, die wie folgt aussieht:

Denken Sie daran, dass die zweite Sitzung immer noch exklusiv ist sperrt an dieser Stelle die Zeilen 1 und 2. Es steht uns frei, bei Bedarf Sperren für Reihe 3 zu erwerben. Die folgende Abfrage ist diejenige, die wir verwenden werden, um das Verhalten mit mehreren Verweisen auf die Zieltabelle anzuzeigen:

-- Multi-reference update test
UPDATE WriteRef
SET Data = ReadRef.Data * 2
OUTPUT 
    ReadRef.RowID, 
    ReadRef.Data,
    INSERTED.RowID AS UpdatedRowID,
    INSERTED.Data AS NewDataValue
FROM dbo.Test AS ReadRef
JOIN dbo.Test AS WriteRef
    ON WriteRef.RowID = ReadRef.RowID + 2
WHERE 
    ReadRef.RowID = 1;

Dies ist eine komplexere Abfrage, aber ihre Bedienung ist relativ einfach. Es gibt zwei Referenzen auf die Testtabelle, eine habe ich als ReadRef und die andere als WriteRef bezeichnet. Die Idee ist lesen von Zeile 1 (unter Verwendung einer Zeilenversion) über ReadRef und zu update die dritte Zeile (die eine Aktualisierungssperre benötigt) mit WriteRef.

Die Abfrage gibt Zeile 1 explizit in der where-Klausel für die Lesetabellenreferenz an. Es verbindet sich mit dem Schreibverweis auf die gleiche Tabelle durch Hinzufügen von 2 zu dieser RowID (also Identifizieren von Zeile 3). Die Update-Anweisung verwendet auch eine Ausgabeklausel, um eine Ergebnismenge zurückzugeben, die die aus der Quelltabelle gelesenen Werte und die resultierenden Änderungen an Zeile 3 anzeigt.

Der geschätzte Abfrageplan für diese Anweisung lautet wie folgt:

Die Eigenschaften der Suche mit der Bezeichnung (1) zeigen, dass diese Suche auf der ReadRef ist Alias, liest Daten aus der Zeile mit RowID 1:

Diese Suchoperation findet keine Zeile, die aktualisiert wird, daher sind Aktualisierungssperren nicht vergriffen; der Lesevorgang wird mit versionierten Daten durchgeführt. Der Lesevorgang wird nicht durch die exklusiven Sperren der anderen Sitzung blockiert.

Der Compute-Skalar mit der Bezeichnung (2) definiert einen Ausdruck mit der Bezeichnung 1004, der den aktualisierten Datenspaltenwert berechnet. Ausdruck 1009 berechnet die zu aktualisierende Reihen-ID (1 + 2 =Reihen-ID 3):

Die zweite Suche ist ein Verweis auf dieselbe Tabelle (3). Diese Suche lokalisiert die zu aktualisierende Zeile (Zeile 3) mit dem Ausdruck 1009:

Da diese Suche eine zu ändernde Zeile findet, eine Aktualisierungssperre statt Zeilenversionen verwendet wird. Es gibt keine widersprüchliche exklusive Sperre auf Zeilen-ID 3, daher wird die Sperranforderung sofort gewährt.

Der letzte hervorgehobene Operator (4) ist der Aktualisierungsvorgang selbst. Die Aktualisierungssperre in Zeile 3 wird auf eine exklusive hochgestuft an dieser Stelle sperren, kurz bevor die Änderung tatsächlich durchgeführt wird. Dieser Operator gibt auch die in der Ausgabeklausel angegebenen Daten zurück der Update-Anweisung:

Das Ergebnis der Update-Anweisung (erzeugt durch die Ausgabeklausel) wird unten gezeigt:

Der endgültige Zustand der Tabelle ist wie folgt:

Wir können die während der Ausführung genommenen Sperren mit einem Profiler-Trace bestätigen:

Dies zeigt, dass nur ein einziges Update Zeilentastensperre wird erworben. Wenn diese Zeile den Aktualisierungsoperator erreicht, wird die Sperre in eine exklusive umgewandelt sperren. Am Ende der Anweisung wird die Sperre aufgehoben.

Möglicherweise können Sie der Ablaufverfolgungsausgabe entnehmen, dass der Sperrhashwert für die Zeile mit Aktualisierungssperre (98ec012aa510) lautet in meiner Testdatenbank. Die folgende Abfrage zeigt, dass dieser Sperr-Hash tatsächlich mit RowID 3 im Clustered-Index verknüpft ist:

SELECT RowID, %%LockRes%%
FROM dbo.Test;

Beachten Sie, dass die in diesen Beispielen verwendeten Update-Sperren von kürzerer Dauer sind als die Update-Sperren, die ergriffen werden, wenn wir einen UPDLOCK angeben Hinweis. Diese internen Update-Sperren werden am Ende der Anweisung aufgehoben, während UPDLOCK Sperren werden bis zum Ende der Transaktion gehalten.

Damit ist die Demonstration von Fällen abgeschlossen, in denen RCSI Aktualisierungssperren erwirbt, um aktuelle festgeschriebene Daten zu lesen, anstatt die Zeilenversionierung zu verwenden.

Shared und Key-Range Locks unter RCSI

Es gibt eine Reihe anderer Szenarien, in denen das Datenbankmodul unter RCSI möglicherweise weiterhin Sperren erwirbt. Diese Situationen beziehen sich alle auf die Notwendigkeit, die Korrektheit zu bewahren, die gefährdet wäre, wenn man sich auf möglicherweise veraltete versionierte Daten verlässt.

Shared Locks für die Fremdschlüsselvalidierung

Bei zwei Tabellen in einer direkten Fremdschlüsselbeziehung muss die Datenbank-Engine Schritte unternehmen, um sicherzustellen, dass Einschränkungen nicht verletzt werden, indem sie sich auf möglicherweise veraltete versionierte Lesevorgänge verlässt. Die aktuelle Implementierung tut dies, indem sie auf Sperren von festgeschriebenen Lesevorgängen umschaltet beim Zugriff auf Daten im Rahmen einer automatischen Fremdschlüsselprüfung.

Das Aufnehmen gemeinsamer Sperren stellt sicher, dass die Integritätsprüfung die allerneuesten festgeschriebenen Daten liest (keine alte Version) oder aufgrund einer gleichzeitigen In-Flight-Änderung blockiert. Der Wechsel zum Sperren von Read Committed gilt nur für die bestimmte Zugriffsmethode, die zum Überprüfen von Fremdschlüsseldaten verwendet wird; andere Datenzugriffe in derselben Anweisung verwenden weiterhin Zeilenversionen.

Dieses Verhalten gilt nur für Anweisungen, die Daten ändern, wobei sich die Änderung direkt auf eine Fremdschlüsselbeziehung auswirkt. Für Änderungen an der referenzierten (übergeordneten) Tabelle bedeutet dies Aktualisierungen, die sich auf den referenzierten Wert auswirken (es sei denn, er ist auf NULL gesetzt). ) und alle Löschungen. Für die referenzierende (untergeordnete) Tabelle bedeutet dies alle Einfügungen und Aktualisierungen (wiederum, es sei denn, die Schlüsselreferenz ist NULL ). Die gleichen Überlegungen gelten für die Komponenteneffekte eines MERGE .

Ein Beispiel für einen Ausführungsplan, der eine Fremdschlüsselsuche zeigt, die gemeinsam genutzte Sperren verwendet, ist unten dargestellt:

Serialisierbar für kaskadierende Fremdschlüssel

Wenn die Fremdschlüsselbeziehung eine kaskadierende Aktion hat, erfordert die Korrektheit eine lokale Eskalation zu einer serialisierbaren Isolationssemantik. Dies bedeutet, dass Schlüsselbereichssperren für eine kaskadierende referenzielle Aktion verwendet werden. Wie bei den zuvor gesehenen Aktualisierungssperren gelten diese Schlüsselbereichssperren für die Anweisung, nicht für die Transaktion. Ein Beispielausführungsplan, der zeigt, wo die internen serialisierbaren Sperren unter RCSI genommen werden, ist unten gezeigt:

Andere Szenarien

Es gibt viele andere spezifische Fälle, in denen die Engine die Lebensdauer von Sperren automatisch verlängert oder die Isolationsstufe lokal eskaliert, um die Korrektheit sicherzustellen. Dazu gehört die serialisierbare Semantik, die beim Verwalten einer verwandten indizierten Ansicht oder beim Verwalten eines Index mit dem IGNORE_DUP_KEY verwendet wird Optionssatz.

Die Erkenntnis zum Mitnehmen ist, dass RCSI die Anzahl der Sperren reduziert, sie aber nicht immer vollständig beseitigen kann.

Nächstes Mal

Der nächste Beitrag in dieser Reihe befasst sich mit der Snapshot-Isolationsstufe.

[Siehe den Index für die gesamte Serie]