it-swarm.com.de

Verhinderung von MERGE-Deadlocks

In einer unserer Datenbanken haben wir eine Tabelle, auf die mehrere Threads gleichzeitig intensiv zugreifen. Threads aktualisieren oder fügen Zeilen über MERGE ein. 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 this 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;

d.h. der Abgleich erfolgt durch einen eindeutigen Indexschlüssel. Hinweis HOLDLOCK ist wegen Parallelität hier (wie empfohlen hier ).

Ich habe kleine Nachforschungen angestellt und Folgendes gefunden.

In den meisten Fällen ist der Abfrageausführungsplan

(index seek execution plan

mit dem folgenden Verriegelungsmuster

(index seek locking pattern

d.h. IX Sperre für das Objekt, gefolgt von detaillierteren Sperren.

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

(table scan execution plan

(Diese Planform kann durch Hinzufügen eines Hinweises INDEX(0) erzwungen werden.) Das Sperrmuster lautet

(table scan locking pattern

beachten Sie, dass X Sperre für Objekt platziert wurde, nachdem IX bereits platziert wurde.

Da zwei IX kompatibel sind, zwei X jedoch nicht, geschieht dies unter Parallelität

(deadlock

(deadlock graph

Sackgasse !

Und hier der erste Teil der Frage entsteht. Ist das Sperren von X für Objekte nach IX zulässig? Ist es nicht ein Fehler?

Dokumentation besagt:

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

und auch

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

das Setzen einer X Sperre für ein Objekt nach IX erscheint mir SEHR verdächtig.

Zuerst habe ich versucht, Deadlocks 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 TABLOCK an Ort und Stelle wird das Sperrmuster

(merge holdlock tablock locking pattern

und mit dem TABLOCKX Sperrmuster ist

(merge holdlock tablockx locking pattern

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

Meine nächsten Versuche waren das Hinzufügen von PAGLOCK und ROWLOCK, um Sperren detaillierter zu gestalten und Konflikte zu reduzieren. Beides hat keine Auswirkung (X auf das Objekt wurde unmittelbar nach IX noch beobachtet).

Mein letzter Versuch bestand darin, eine "gute" Form des Ausführungsplans mit einer guten granularen Verriegelung durch Hinzufügen eines FORCESEEK-Hinweises zu erzwingen

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

und es hat funktioniert.

Und hier der zweite Teil der Frage entsteht. Könnte es passieren, dass FORCESEEK ignoriert wird und ein schlechtes Sperrmuster verwendet wird? (Wie ich bereits erwähnte, wurden PAGLOCK und ROWLOCK scheinbar ignoriert).


Das Hinzufügen von UPDLOCK hat keine Auswirkung (X auf Objekte, die nach IX noch sichtbar sind).

Es hat funktioniert, den IX_Cache - Index wie erwartet zu gruppieren. Es führte zu einem Plan mit Clustered Index Seek und einer granularen Verriegelung. Zusätzlich habe ich versucht, Clustered Index Scan zu erzwingen, das auch granulares Sperren zeigte.

Jedoch. Zusätzliche Beobachtung. Im ursprünglichen Setup sogar mit FORCESEEK(IX_Cache(ItemKey))), wenn eine @itemKey - Variablendeklaration von varchar (200) in nvarchar (200) geändert wird. , Ausführungsplan wird

(index seek execution plan with nvarchar

sehen Sie, dass die Suche verwendet wird. ABER das Sperrmuster zeigt in diesem Fall erneut die Sperre X, die nach IX für das Objekt platziert wurde.

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, X eine Sperre für ein Objekt nach IX zu setzen, ist noch offen. Und wenn es berechtigt ist, gibt es etwas, das man tun kann, um das Sperren von Objekten zu verhindern?

8
i-one

Ist das Platzieren von IX gefolgt von X auf dem Objekt zulässig? Ist es ein Fehler oder nicht?

Es sieht ein bisschen seltsam aus, ist aber gültig. Zu dem Zeitpunkt, an dem IX genommen wird, kann die Absicht durchaus darin bestehen, X Sperren 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 Ebenen übersprungen werden können (ein Beispiel für Sperren IS und S ist hier zu sehen).

Insbesondere für das aktuelle Szenario ist es wahr, dass für einen Heap keine serialisierbaren Schlüsselbereichssperren verfügbar sind. Daher ist die einzige Alternative eine X -Sperre auf Objektebene. In diesem Sinne kann die Engine möglicherweise frühzeitig erkennen, dass eine X -Sperre zwangsläufig erforderlich ist, wenn die Zugriffsmethode ein Heap-Scan ist, und daher die IX -Sperre 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 von IX ist möglicherweise die am wenigsten invasive Methode, um einen erforderlichen Schutz für einen obskuren Edge-Fall bereitzustellen. 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 FORCESEEK ignoriert wird und ein schlechtes Sperrmuster verwendet wird?

Nein. FORCESEEK ist weniger ein Hinweis als eine Direktive. 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 für Zeilen zum Ändern 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. Um dies zu umgehen, machen Sie die Garantie mit MERGE TOP (1) INTO [Cache]... explizit.

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. eine Plananleitung, aber der Motor kann immer noch entscheiden, zur Laufzeit andere Sperren zu nehmen. Die Chancen sind ziemlich gering, wenn Sie das obige Element TOP (1) einbinden.

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.

MERGE ist auch in einer Umgebung mit hoher Parallelität etwas ungewöhnlich. Etwas kontraintuitiv ist es oft effizienter, separate INSERT - und UPDATE -Anweisungen 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:

(Execution plan

Wenn Sie die Existenz eines eindeutigen Index für ItemKey (wie in der Frage) garantieren können, kann die redundante TOP (1) in UPDATE entfernt werden, was den einfacheren Plan ergibt:

(Simplified update

Sowohl INSERT als auch UPDATE Pläne qualifizieren sich in beiden Fällen für einen trivialen Plan. MERGE erfordert immer eine vollständige kostenbasierte Optimierung.

In den zugehörigen Fragen und Antworten SQL Server 2014 Concurrent Input Issue finden Sie das richtige zu verwendende Muster sowie weitere Informationen zu MERGE.

Deadlocks können nicht immer verhindert werden. Sie können durch sorgfältige Codierung und sorgfältiges Design 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 in SQL Server Concurrent Inserts and Deletes beschrieben.

8
Paul White 9