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

Read Committed Snapshot Isolation

[Siehe den Index für die gesamte Serie]

SQL Server bietet zwei physische Implementierungen des read commit Isolationsstufe definiert durch den SQL-Standard, Sperren von Read Committed und Read Committed Snapshot Isolation (RCSI ). Während beide Implementierungen die Anforderungen erfüllen, die im SQL-Standard für Read-Committed-Isolationsverhalten festgelegt sind, hat RCSI ein ganz anderes physisches Verhalten als die Locking-Implementierung, die wir uns im vorherigen Beitrag dieser Serie angesehen haben.

Logische Garantien

Der SQL-Standard erfordert, dass eine Transaktion, die auf der Read-Committed-Isolationsstufe ausgeführt wird, keine Dirty Reads erfährt. Eine andere Möglichkeit, diese Anforderung auszudrücken, besteht darin, zu sagen, dass eine Transaktion mit Lese-Commit nur auf Commit-Daten stoßen darf .

Der Standard besagt auch, dass das Lesen von festgeschriebenen Transaktionen möglicherweise ist erleben die Nebenläufigkeitsphänomene, die als nicht wiederholbare Lesevorgänge und Phantome bekannt sind (obwohl sie dazu eigentlich nicht erforderlich sind). Zufälligerweise kann es bei beiden physischen Implementierungen der Read-Committed-Isolation in SQL Server zu nicht wiederholbaren Lesevorgängen und Phantomzeilen kommen, obwohl die genauen Details sehr unterschiedlich sind.

Eine Point-in-Time-Ansicht von festgeschriebenen Daten

Wenn die Datenbankoption READ_COMMITTED_SNAPSHOT in ON , verwendet SQL Server eine Zeilenversionsverwaltungsimplementierung der Read Committed-Isolationsstufe. Wenn diese Option aktiviert ist, verwenden Transaktionen, die eine Read-Committed-Isolation anfordern, automatisch die RCSI-Implementierung. Für die Verwendung von RCSI sind keine Änderungen am vorhandenen T-SQL-Code erforderlich. Beachten Sie jedoch sorgfältig, dass dies nicht dasselbe ist wie gesagt, dass sich der Code gleich verhält unter RCSI wie bei Verwendung der Locking-Implementierung von Read Committed, tatsächlich ist dies ganz allgemein nicht der Fall .

Nichts im SQL-Standard verlangt, dass die Daten, die von einer Read-Committed-Transaktion gelesen werden, die neuesten sein müssen verbindliche Daten. Die RCSI-Implementierung von SQL Server nutzt dies, um Transaktionen mit einer Point-in-Time-Ansicht bereitzustellen von festgeschriebenen Daten, wobei dieser Zeitpunkt der Moment ist, in dem die aktuelle Anweisung begonnen hat Ausführung (nicht in dem Moment, in dem eine enthaltende Transaktion gestartet wurde).

Dies unterscheidet sich deutlich vom Verhalten der SQL Server-Sperrimplementierung von Read Committed, wo die Anweisung die zuletzt festgeschriebenen Daten ab dem Moment, in dem jedes Element physisch gelesen wird, sieht . Durch das Sperren von Read Committed werden gemeinsam genutzte Sperren so schnell wie möglich aufgehoben, sodass die angetroffenen Datensätze von sehr unterschiedlichen Zeitpunkten stammen können.

Zusammenfassend lässt sich sagen, dass das Sperren von Read Commited jede Zeile sieht wie es damals war, wurde es kurz verschlossen und physisch gelesen; RCSI sieht alle Zeilen wie sie zu Beginn der Aussage waren. Beide Implementierungen sehen garantiert niemals nicht festgeschriebene Daten, aber die Daten, auf die sie stoßen, können sehr unterschiedlich sein.

Die Auswirkungen einer Point-in-Time-Ansicht

Das Anzeigen einer Point-in-Time-Ansicht von festgeschriebenen Daten scheint dem komplexeren Verhalten der Sperrimplementierung selbstverständlich überlegen zu sein. Es ist beispielsweise klar, dass eine Point-in-Time-Ansicht nicht unter den Problemen fehlender Zeilen leiden kann oder mehrere Male auf dieselbe Reihe stoßen , die beide unter Locking Read Committed Isolation möglich sind.

Ein zweiter wichtiger Vorteil von RCSI ist, dass es keine gemeinsamen Sperren erwirbt beim Lesen von Daten, da die Daten aus dem Zeilenversionsspeicher stammen und nicht direkt darauf zugegriffen wird. Das Fehlen gemeinsamer Sperren kann die Parallelität erheblich verbessern durch Eliminieren von Konflikten mit gleichzeitigen Transaktionen, die darauf abzielen, inkompatible Sperren zu erwerben. Dieser Vorteil wird üblicherweise so zusammengefasst, dass Leser Schreiber unter RCSI nicht blockieren und umgekehrt. Als weitere Folge der Verringerung der Blockierung aufgrund inkompatibler Sperranforderungen besteht die Möglichkeit für Deadlocks wird normalerweise stark reduziert, wenn es unter RCSI läuft.

Diese Vorteile gehen jedoch nicht ohne Kosten und Vorbehalte einher . Zum einen verbraucht das Verwalten von Versionen festgeschriebener Zeilen Systemressourcen, daher ist es wichtig, dass die physische Umgebung so konfiguriert ist, dass sie damit fertig wird, hauptsächlich in Bezug auf tempdb Leistungs- und Speicher-/Festplattenanforderungen.

Der zweite Vorbehalt ist etwas subtiler:RCSI bietet eine Momentaufnahme der übertragenen Daten wie sie waren am Anfang der Anweisung, aber es gibt nichts, was verhindert, dass die echten Daten geändert (und diese Änderungen festgeschrieben) werden, während die RCSI-Anweisung ausgeführt wird. Denken Sie daran, dass es keine gemeinsamen Sperren gibt. Eine unmittelbare Folge dieses zweiten Punktes ist, dass T-SQL-Code, der unter RCSI ausgeführt wird, Entscheidungen basierend auf veralteten Informationen treffen kann , verglichen mit dem aktuellen festgeschriebenen Zustand der Datenbank. Wir werden in Kürze mehr darüber sprechen.

Es gibt eine letzte (implementierungsspezifische) Beobachtung, die ich über RCSI machen möchte, bevor wir fortfahren. Skalare Funktionen und Funktionen mit mehreren Anweisungen mit einem anderen internen T-SQL-Kontext als der enthaltenden Anweisung ausführen. Dies bedeutet, dass die Point-in-Time-Ansicht, die in einem Skalar- oder Funktionsaufruf mit mehreren Anweisungen zu sehen ist, später sein kann als die Point-in-Time-Ansicht, die vom Rest der Anweisung gesehen wird. Dies kann zu unerwarteten Inkonsistenzen führen, da verschiedene Teile derselben Anweisung Daten von verschiedenen Zeitpunkten sehen . Dieses seltsame und verwirrende Verhalten ist nicht gelten für Inline-Funktionen, die denselben Snapshot sehen wie die Anweisung, in der sie erscheinen.

Nicht wiederholbare Lesevorgänge und Phantome

Bei einer Point-in-Time-Ansicht auf Anweisungsebene des festgeschriebenen Zustands der Datenbank ist es möglicherweise nicht sofort ersichtlich, wie bei einer lesefestgeschriebenen Transaktion unter RCSI das nicht wiederholbare Lese- oder Phantomzeilenphänomen auftreten kann. In der Tat, wenn wir unser Denken auf den Umfang einer einzelnen Aussage beschränken , ist keines dieser Phänomene unter RCSI möglich.

Dieselben Daten mehrmals innerhalb derselben Anweisung lesen unter RCSI immer die gleichen Datenwerte zurückgibt, zwischen diesen Lesevorgängen verschwinden keine Daten und es erscheinen auch keine neuen Daten. Wenn Sie sich fragen, welche Art von Anweisung dieselben Daten mehr als einmal lesen könnte, denken Sie an Abfragen, die dieselbe Tabelle mehr als einmal referenzieren, vielleicht in einer Unterabfrage.

Die Lesekonsistenz auf Anweisungsebene ist eine offensichtliche Folge der Lesevorgänge, die für einen festen Snapshot der Daten ausgegeben werden. Der Grund, warum RCSI nicht Schutz vor nicht wiederholbaren Lesevorgängen und Phantomen bieten, liegt darin, dass diese SQL-Standardphänomene auf Transaktionsebene definiert werden. Mehrere Anweisungen innerhalb einer Transaktion, die bei RCSI ausgeführt wird, sehen möglicherweise unterschiedliche Daten, da jede Anweisung eine Point-in-Time-Ansicht ab dem Moment dieser bestimmten Anweisung sieht gestartet.

Zusammenfassend jede Aussage innerhalb einer RCSI-Transaktion sieht einen statischen festgeschriebenen Datensatz, aber dieser Satz kann sich zwischen Anweisungen innerhalb derselben Transaktion ändern.

Veraltete Daten

Die Möglichkeit, dass unser T-SQL-Code eine wichtige Entscheidung auf der Grundlage veralteter Informationen trifft, ist mehr als nur ein wenig beunruhigend. Bedenken Sie für einen Moment, dass der zeitpunktbezogene Snapshot, der von einer einzelnen Anweisung verwendet wird, die unter RCSI ausgeführt wird, möglicherweise willkürlich alt ist .

Eine Anweisung, die über einen beträchtlichen Zeitraum ausgeführt wird, wird weiterhin den festgeschriebenen Zustand der Datenbank sehen, wie er zu Beginn der Anweisung war. In der Zwischenzeit fehlen der Anweisung alle festgeschriebenen Änderungen, die seit dieser Zeit in der Datenbank aufgetreten sind.

Das soll nicht heißen, dass Probleme im Zusammenhang mit dem Zugriff auf veraltete Daten unter RCSI auf dauerhaft beschränkt sind Aussagen, aber die Probleme könnten in solchen Fällen sicherlich ausgeprägter sein.

Eine Frage des Timings

Dieses Problem mit veralteten Daten gilt im Prinzip für alle RCSI-Aussagen, egal wie schnell sie abgeschlossen sein mögen. Wie klein das Zeitfenster auch sein mag, es besteht immer die Möglichkeit, dass eine gleichzeitige Operation den Datensatz, mit dem wir arbeiten, ändert, ohne dass wir uns dieser Änderung bewusst sind. Schauen wir uns noch einmal eines der einfachen Beispiele an, die wir zuvor verwendet haben, als wir das Verhalten des Sperrens von Read Committed untersucht haben:

INSERT dbo.OverdueInvoices
SELECT I.InvoiceNumber
FROM dbo.Invoices AS I
WHERE I.TotalDue >
(
    SELECT SUM(P.Amount)
    FROM dbo.Payments AS P
    WHERE P.InvoiceNumber = I.InvoiceNumber
);

Wenn sie unter RCSI ausgeführt wird, kann diese Anweisung nicht Sehen Sie sich alle festgeschriebenen Datenbankänderungen an, die nach Beginn der Ausführung der Anweisung auftreten. Wir werden zwar nicht auf die Probleme von verpassten oder mehrfach auftretenden Zeilen stoßen, die unter der Sperrimplementierung möglich sind, aber eine gleichzeitige Transaktion könnte eine Zahlung hinzufügen, die sollte um zu verhindern, dass ein Kunde eine strenge Mahnung wegen einer überfälligen Zahlung erhält, nachdem die obige Anweisung ausgeführt wurde.

Sie können sich wahrscheinlich viele andere potenzielle Probleme vorstellen, die in diesem Szenario oder in anderen Szenarios auftreten können, die konzeptionell ähnlich sind. Je länger die Anweisung ausgeführt wird, desto veralteter wird ihre Ansicht der Datenbank und desto größer ist der Spielraum für möglicherweise unbeabsichtigte Folgen.

Natürlich gibt es in diesem speziellen Beispiel viele mildernde Faktoren. Das Verhalten kann durchaus als vollkommen akzeptabel angesehen werden. Denn eine Mahnung zu versenden, weil eine Zahlung ein paar Sekunden zu spät eingetroffen ist, ist eine leicht zu verteidigende Handlung. Das Prinzip bleibt jedoch bestehen.

Versagen von Geschäftsregeln und Integritätsrisiken

Aus der Verwendung veralteter Informationen können schwerwiegendere Probleme entstehen, als ein paar Sekunden zu früh eine Abmahnung zu versenden. Ein gutes Beispiel für diese Klasse von Schwachstellen ist der Triggercode Wird verwendet, um eine Integritätsregel durchzusetzen, die möglicherweise zu komplex ist, um sie mit deklarativen referenziellen Integritätsbedingungen durchzusetzen. Betrachten Sie zur Veranschaulichung den folgenden Code, der einen Trigger verwendet, um eine Variation einer Fremdschlüsseleinschränkung zu erzwingen, aber einen, der die Beziehung nur für bestimmte untergeordnete Tabellenzeilen erzwingt:

ALTER DATABASE Sandpit
SET READ_COMMITTED_SNAPSHOT ON
WITH ROLLBACK IMMEDIATE;
GO
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
GO
CREATE TABLE dbo.Parent (ParentID integer PRIMARY KEY);
GO
CREATE TABLE dbo.Child
(
    ChildID integer IDENTITY PRIMARY KEY,
    ParentID integer NOT NULL,
    CheckMe bit NOT NULL
);
GO
CREATE TRIGGER dbo.Child_AI
ON dbo.Child
AFTER INSERT
AS
BEGIN
    -- Child rows with CheckMe = true
    -- must have an associated parent row
    IF EXISTS
    (
        SELECT ins.ParentID
        FROM inserted AS ins
        WHERE ins.CheckMe = 1
        EXCEPT
        SELECT P.ParentID
        FROM dbo.Parent AS P
    )
    BEGIN
    	RAISERROR ('Integrity violation!', 16, 1);
        ROLLBACK TRANSACTION;
    END
END;
GO
-- Insert parent row #1
INSERT dbo.Parent (ParentID) VALUES (1);

Stellen Sie sich nun eine Transaktion vor, die in einer anderen Sitzung ausgeführt wird (verwenden Sie dafür ein anderes SSMS-Fenster, wenn Sie mitmachen), die die übergeordnete Zeile #1 löscht, aber noch keinen Commit durchführt:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRANSACTION;
DELETE FROM dbo.Parent
WHERE ParentID = 1;

Zurück in unserer ursprünglichen Sitzung versuchen wir, eine (markierte) Kindzeile einzufügen, die auf dieses Elternteil verweist:

INSERT dbo.Child (ParentID, CheckMe)
VALUES (1, 1);

Der Triggercode wird ausgeführt, aber da RCSI nur committed sieht Daten zu dem Zeitpunkt, an dem die Anweisung gestartet wurde, sieht sie immer noch die übergeordnete Zeile (nicht die nicht festgeschriebene Löschung) und die Einfügung ist erfolgreich !

Die Transaktion, die die übergeordnete Zeile gelöscht hat, kann ihre Änderung jetzt erfolgreich festschreiben, wodurch die Datenbank in einem inkonsistenten Zustand verbleibt Zustand in Bezug auf unsere Auslöselogik:

COMMIT TRANSACTION;
SELECT P.* FROM dbo.Parent AS P;
SELECT C.* FROM dbo.Child AS C;

Dies ist natürlich ein vereinfachtes Beispiel, das mit den eingebauten Beschränkungseinrichtungen leicht umgangen werden könnte. Viel komplexere Geschäftsregeln und Pseudo-Integritätseinschränkungen können innerhalb und außerhalb von Triggern geschrieben werden . Das Potenzial für fehlerhaftes Verhalten unter RCSI sollte offensichtlich sein.

Blockierungsverhalten und zuletzt festgeschriebene Daten

Ich habe bereits erwähnt, dass sich T-SQL-Code unter RCSI-Read-Committed nicht garantiert genauso verhält wie unter Verwendung der Locking-Implementierung. Das obige Trigger-Codebeispiel ist eine gute Illustration dafür, aber ich muss betonen, dass das allgemeine Problem nicht auf Trigger beschränkt ist .

RCSI ist in der Regel keine gute Wahl für T-SQL-Code, dessen Korrektheit vom Blockieren abhängt, wenn eine gleichzeitige nicht festgeschriebene Änderung vorhanden ist. RCSI ist möglicherweise auch nicht die richtige Wahl, wenn der Code vom Lesen von aktuell abhängt festgeschriebene Daten und nicht die letzten festgeschriebenen Daten zum Zeitpunkt des Beginns der Anweisung. Diese beiden Überlegungen hängen zusammen, sind aber nicht dasselbe.

Locking Read Committed unter RCSI

SQL Server bietet eine Möglichkeit, Sperren anzufordern Read Committed, wenn RCSI aktiviert ist, unter Verwendung des Tabellenhinweises READCOMMITTEDLOCK . Wir können unseren Trigger ändern, um die oben gezeigten Probleme zu vermeiden, indem wir diesen Hinweis zur Tabelle hinzufügen, der das Blockierungsverhalten benötigt, um korrekt zu funktionieren:

ALTER TRIGGER dbo.Child_AI
ON dbo.Child
AFTER INSERT
AS
BEGIN
    -- Child rows with CheckMe = true
    -- must have an associated parent row
    IF EXISTS
    (
        SELECT ins.ParentID
        FROM inserted AS ins
        WHERE ins.CheckMe = 1
        EXCEPT
        SELECT P.ParentID
        FROM dbo.Parent AS P WITH (READCOMMITTEDLOCK) -- NEW!!
    )
    BEGIN
        RAISERROR ('Integrity violation!', 16, 1);
        ROLLBACK TRANSACTION;
    END
END;

Mit dieser Änderung wird versucht, die potenziell verwaisten untergeordneten Zeilenblöcke einzufügen, bis die Löschtransaktion festgeschrieben (oder abgebrochen) wird. Wenn der Löschvorgang ausgeführt wird, erkennt der Auslösecode die Integritätsverletzung und löst den erwarteten Fehler aus.

Identifizieren von Abfragen, die möglicherweise nicht richtig ausgeführt werden unter RCSI ist eine nicht triviale Aufgabe, die umfangreiche Tests erfordern kann um es richtig zu machen (und denken Sie bitte daran, dass diese Probleme ziemlich allgemein sind und nicht auf Triggercode beschränkt sind!) Außerdem das Hinzufügen des READCOMMITTEDLOCK Hinweis zu jeder Tabelle, die es benötigt, kann ein mühsamer und fehleranfälliger Prozess sein. Bis SQL Server eine breiter angelegte Option bietet, um die Sperrimplementierung bei Bedarf anzufordern, bleiben wir bei der Verwendung der Tabellenhinweise hängen.

Nächstes Mal

Der nächste Beitrag in dieser Reihe setzt unsere Untersuchung der Read-Committed-Snapshot-Isolation fort und wirft einen Blick auf das überraschende Verhalten von Datenänderungsanweisungen unter RCSI.

[Siehe den Index für die gesamte Serie]