Verhinderung von MERGE-Deadlocks

9

In einer unserer Datenbanken haben wir eine Tabelle, auf die mehrere Threads gleichzeitig intensiv zugreifen. Threads aktualisieren oder fügen Zeilen über ein MERGE. Es gibt auch Threads, die gelegentlich Zeilen löschen, sodass Tabellendaten sehr flüchtig sind. Threads, die Upsts machen, leiden manchmal unter Deadlocking. Das Problem ähnelt dem in dieser Frage beschriebenen. Der Unterschied besteht jedoch darin, dass in unserem Fall jeder Thread genau eine Zeile aktualisiert oder einfügt .

Es folgt eine vereinfachte Einrichtung. Die Tabelle ist ein Heap mit zwei eindeutigen nicht gruppierten Indizes

CREATE TABLE [Cache]
(
    [UID] uniqueidentifier NOT NULL CONSTRAINT DF_Cache_UID DEFAULT (newid()),
    [ItemKey] varchar(200) NOT NULL,
    [FileName] nvarchar(255) NOT NULL,
    [Expires] datetime2(2) NOT NULL,
    CONSTRAINT [PK_Cache] PRIMARY KEY NONCLUSTERED ([UID])
)
GO
CREATE UNIQUE INDEX IX_Cache ON [Cache] ([ItemKey]);
GO

und die typische Abfrage ist

DECLARE
    @itemKey varchar(200) = 'Item_0F3C43A6A6A14255B2EA977EA730EDF2',
    @fileName nvarchar(255) = 'File_0F3C43A6A6A14255B2EA977EA730EDF2.dat';

MERGE INTO [Cache] WITH (HOLDLOCK) T
USING (
    VALUES (@itemKey, @fileName, dateadd(minute, 10, sysdatetime()))
) S(ItemKey, FileName, Expires)
ON T.ItemKey = S.ItemKey
WHEN MATCHED THEN
    UPDATE
    SET
        T.FileName = S.FileName,
        T.Expires = S.Expires
WHEN NOT MATCHED THEN
    INSERT (ItemKey, FileName, Expires)
    VALUES (S.ItemKey, S.FileName, S.Expires)
OUTPUT deleted.FileName;

Das heißt, der Abgleich erfolgt über einen eindeutigen Indexschlüssel. Hinweis HOLDLOCKist hier wegen Parallelität (wie hier empfohlen ).

Ich habe kleine Nachforschungen angestellt und Folgendes gefunden.

In den meisten Fällen ist der Abfrageausführungsplan

Ausführungsplan für die Indexsuche

mit dem folgenden Verriegelungsmuster

Indexsuch-Sperrmuster

dh IXSperren des Objekts, gefolgt von detaillierteren Sperren.

Manchmal ist der Ausführungsplan für Abfragen jedoch anders

Ausführungsplan für den Tabellenscan

(Diese Planform kann durch Hinzufügen eines INDEX(0)Hinweises erzwungen werden ) und das Sperrmuster ist

Sperrmuster für Tabellenscan

Hinweis XSperre auf Objekt IXplatziert, nachdem bereits platziert wurde.

Da zwei IXkompatibel sind, zwei Xjedoch nicht, geschieht dies unter Parallelität

Sackgasse

Deadlock-Diagramm

Deadlock !

Und hier stellt sich der erste Teil der Frage . Ist das XSperren eines Objekts nach der IXBerechtigung zulässig? Ist es nicht ein Fehler?

Dokumentationszustände :

Absichtssperren werden als Absichtssperren bezeichnet, da sie vor einer Sperre auf der unteren Ebene erfasst werden und daher die Absicht signalisieren, Sperren auf einer niedrigeren Ebene zu platzieren .

und auch

IX bedeutet die Absicht, nur einige der Zeilen und nicht alle zu aktualisieren

so, platzieren XSperre auf Objekt nach IXAussehen mir sehr verdächtig.

Zuerst habe ich versucht, Deadlocking zu verhindern, indem ich versucht habe, Hinweise zum Sperren von Tabellen hinzuzufügen

MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCK) T

und

MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCKX) T

mit dem TABLOCKan Ort und Stelle wird das Verriegelungsmuster

Zusammenführen des Holdlock-Tablock-Sperrmusters

und mit dem TABLOCKXVerriegelungsmuster ist

Holdlock Tablockx-Sperrmuster zusammenführen

Da zwei SIX(sowie zwei X) nicht kompatibel sind, verhindert dies effektiv Deadlocks, aber leider auch Parallelität (was nicht erwünscht ist).

Meine nächsten Versuche waren das Hinzufügen PAGLOCKund ROWLOCKVerbessern von Sperren und das Reduzieren von Konflikten. Beides hat keine Auswirkung ( Xauf das Objekt wurde noch unmittelbar danach beobachtet IX).

Mein letzter Versuch bestand darin, eine "gute" Ausführungsplanform mit einer guten granularen Verriegelung durch Hinzufügen eines FORCESEEKHinweises zu erzwingen

MERGE INTO [Cache] WITH (HOLDLOCK, FORCESEEK(IX_Cache(ItemKey))) T

und es hat funktioniert.

Und hier stellt sich der zweite Teil der Frage . Könnte es passieren, dass FORCESEEKdies ignoriert wird und ein schlechtes Sperrmuster verwendet wird? (Wie ich bereits erwähnte PAGLOCKund ROWLOCKscheinbar ignoriert wurde).


Das Hinzufügen UPDLOCKhat keine Auswirkung ( Xauf das Objekt, das danach noch sichtbar ist IX).

Das IX_Cacheerwartete Clustering des Index hat funktioniert. Dies führte zu einem Plan mit Clustered Index Seek und granularem Sperren. Zusätzlich habe ich versucht, Clustered Index Scan zu erzwingen , bei dem auch granulares Sperren angezeigt wurde.

Jedoch. Zusätzliche Beobachtung. Im ursprünglichen Setup wird der Ausführungsplan auch dann ausgeführt FORCESEEK(IX_Cache(ItemKey))), wenn eine @itemKeyVariablendeklaration von varchar (200) in nvarchar (200) geändert wird

Index-Suchausführungsplan mit nvarchar

Sehen Sie, dass die Suche verwendet wird. ABER das Sperrmuster zeigt in diesem Fall erneut die XSperre, die nach dem Objekt platziert wurde IX.

Es scheint also, dass das Erzwingen der Suche nicht unbedingt granulare Sperren garantiert (und daher das Fehlen von Deadlocks). Ich bin nicht sicher, ob ein Clustered-Index eine granulare Sperrung garantiert. Oder doch?

Mein Verständnis (korrigieren Sie mich, wenn ich falsch liege) ist, dass das Sperren in hohem Maße situativ ist und eine bestimmte Form des Ausführungsplans kein bestimmtes Sperrmuster impliziert.

Die Frage nach der Berechtigung, Xein Objekt zu sperren, nachdem es IXnoch geöffnet ist. Und wenn es berechtigt ist, gibt es etwas, das man tun kann, um das Sperren von Objekten zu verhindern?

ich ein
quelle
Verwandte Anfrage bei feedback.azure.com
i-one

Antworten:

9

Ist die Platzierung IXgefolgt von Xder Platzierung auf dem Objekt zulässig? Ist es ein Fehler oder nicht?

Es sieht ein bisschen seltsam aus, ist aber gültig. Zum Zeitpunkt der IXAufnahme kann die Absicht durchaus darin bestehen, XSperren auf einer niedrigeren Ebene zu nehmen. Es gibt nichts zu sagen, dass solche Schlösser tatsächlich genommen werden müssen. Schließlich gibt es auf der unteren Ebene möglicherweise nichts zu sperren. Der Motor kann das nicht im Voraus wissen. Darüber hinaus kann es Optimierungen geben, bei denen Sperren niedrigerer Ebene übersprungen werden können (ein Beispiel für ISund SSperren finden Sie hier ).

Insbesondere für das aktuelle Szenario ist es wahr, dass serialisierbare Schlüsselbereichssperren für einen Heap nicht verfügbar sind. Daher ist die einzige Alternative eine XSperre auf Objektebene. In diesem Sinne kann die Engine möglicherweise frühzeitig erkennen, dass eine XSperre zwangsläufig erforderlich ist, wenn es sich bei der Zugriffsmethode um einen Heap-Scan handelt, und daher die IXSperre vermeiden .

Andererseits ist das Sperren komplex, und Absichtssperren können manchmal aus internen Gründen vorgenommen werden, die nicht unbedingt mit der Absicht zusammenhängen, Sperren niedrigerer Ebene zu verwenden. Die Einnahme IXkann die am wenigsten invasive Methode sein, um einen erforderlichen Schutz für einen Fall mit dunkler Kante zu bieten. Eine ähnliche Überlegung finden Sie unter Gemeinsame Sperre für IsolationLevel.ReadUncommitted .

Die aktuelle Situation ist für Ihr Deadlock-Szenario unglücklich und kann im Prinzip vermieden werden, aber das ist nicht unbedingt dasselbe wie ein "Bug". Sie können das Problem über Ihren normalen Support-Kanal oder über Microsoft Connect melden, wenn Sie eine endgültige Antwort darauf benötigen.

Könnte es passieren, dass FORCESEEKdies ignoriert wird und ein schlechtes Sperrmuster verwendet wird?

Nein FORCESEEKist weniger ein Hinweis als eine Richtlinie. Wenn der Optimierer keinen Plan findet, der den "Hinweis" berücksichtigt, wird ein Fehler ausgegeben.

Durch Erzwingen des Index wird sichergestellt, dass Sperren für Schlüsselbereiche ausgeführt werden können. Zusammen mit den Aktualisierungssperren, die natürlich bei der Verarbeitung einer Zugriffsmethode zum Ändern von Zeilen verwendet werden, bietet dies eine ausreichende Garantie, um Parallelitätsprobleme in Ihrem Szenario zu vermeiden.

Wenn sich das Schema der Tabelle nicht ändert (z. B. Hinzufügen eines neuen Index), reicht der Hinweis auch aus, um zu verhindern, dass diese Abfrage mit sich selbst blockiert. Es besteht weiterhin die Möglichkeit eines zyklischen Deadlocks mit anderen Abfragen, die möglicherweise auf den Heap vor dem nicht gruppierten Index zugreifen (z. B. eine Aktualisierung des Schlüssels des nicht gruppierten Index).

... Variablendeklaration von varchar(200)bis nvarchar(200)...

Dies unterbricht die Garantie, dass eine einzelne Reihe betroffen ist, sodass eine Eager-Tischspule zum Schutz vor Halloween eingeführt wird. Machen Sie als weitere Problemumgehung die Garantie mit explizit MERGE TOP (1) INTO [Cache]....

Mein Verständnis [...] ist, dass das Sperren in hohem Maße situativ ist und eine bestimmte Form des Ausführungsplans kein bestimmtes Sperrmuster impliziert.

In einem Ausführungsplan ist sicherlich noch viel mehr los. Sie können eine bestimmte Planform mit z. B. einer Planführung erzwingen, der Motor entscheidet sich jedoch möglicherweise zur Laufzeit für andere Sperren. Die Chancen sind ziemlich gering, wenn Sie das TOP (1)obige Element einbeziehen .

Allgemeine Bemerkungen

Es ist etwas ungewöhnlich, dass eine Heap-Tabelle auf diese Weise verwendet wird. Sie sollten die Vorteile der Konvertierung in eine Clustertabelle in Betracht ziehen, möglicherweise unter Verwendung des Index, den Dan Guzman in einem Kommentar vorgeschlagen hat:

CREATE UNIQUE CLUSTERED INDEX IX_Cache ON [Cache] ([ItemKey]);

Dies kann wichtige Vorteile bei der Wiederverwendung von Speicherplatz haben und eine gute Problemumgehung für das aktuelle Deadlocking-Problem bieten.

MERGEist auch in einer Umgebung mit hoher Parallelität etwas ungewöhnlich. Etwas kontraintuitiv ist es oft effizienter, separate Anweisungen INSERTund UPDATEAnweisungen auszuführen , zum Beispiel:

DECLARE
    @itemKey varchar(200) = 'Item_0F3C43A6A6A14255B2EA977EA730EDF2',
    @fileName nvarchar(255) = 'File_0F3C43A6A6A14255B2EA977EA730EDF2.dat';

BEGIN TRANSACTION;

    DECLARE @expires datetime2(2) = DATEADD(MINUTE, 10, SYSDATETIME());

    UPDATE TOP (1) dbo.Cache WITH (SERIALIZABLE, UPDLOCK)
    SET [FileName] = @fileName,
        Expires = @expires
    OUTPUT Deleted.[FileName]
    WHERE
        ItemKey = @itemKey;

    IF @@ROWCOUNT = 0
        INSERT dbo.Cache
            (ItemKey, [FileName], Expires)
        VALUES
            (@itemKey, @fileName, @expires);

COMMIT TRANSACTION;

Beachten Sie, dass die RID-Suche nicht mehr erforderlich ist:

Ausführungsplan

Wenn Sie die Existenz eines eindeutigen Index für garantieren können ItemKey(wie in der Frage) die redundanten TOP (1)in der UPDATEentfernt werden kann, desto einfachen Plan zu geben:

Vereinfachtes Update

Beide INSERTund UPDATEPläne qualifizieren sich in beiden Fällen für einen trivialen Plan. MERGEerfordert immer eine vollständige kostenbasierte Optimierung.

Das richtige zu verwendende Muster und weitere Informationen finden Sie im zugehörigen Problem mit der gleichzeitigen Eingabe von SQL Server 2014 für gleichzeitige EingabenMERGE .

Deadlocks können nicht immer verhindert werden. Sie können durch sorgfältige Codierung und Gestaltung auf ein Minimum reduziert werden. Die Anwendung sollte jedoch immer darauf vorbereitet sein, den ungeraden Deadlock ordnungsgemäß zu handhaben (z. B. Bedingungen erneut prüfen und dann erneut versuchen).

Wenn Sie die vollständige Kontrolle über die Prozesse haben, die auf das betreffende Objekt zugreifen, können Sie auch Anwendungssperren verwenden, um den Zugriff auf einzelne Elemente zu serialisieren, wie unter SQL Server Concurrent Inserts and Deletes beschrieben .

Paul White 9
quelle