Merge-Anweisung blockiert sich selbst

22

Ich habe das folgende Verfahren (SQL Server 2008 R2):

create procedure usp_SaveCompanyUserData
    @companyId bigint,
    @userId bigint,
    @dataTable tt_CoUserdata readonly
as
begin

    set nocount, xact_abort on;

    merge CompanyUser with (holdlock) as r
    using (
        select 
            @companyId as CompanyId, 
            @userId as UserId, 
            MyKey, 
            MyValue
        from @dataTable) as newData
    on r.CompanyId = newData.CompanyId
        and r.UserId = newData.UserId
        and r.MyKey = newData.MyKey
    when not matched then
        insert (CompanyId, UserId, MyKey, MyValue) values
        (@companyId, @userId, newData.MyKey, newData.MyValue);

end;

CompanyId, UserId, MyKey bilden den zusammengesetzten Schlüssel für die Zieltabelle. CompanyId ist ein Fremdschlüssel für eine übergeordnete Tabelle. Es gibt auch einen nicht gruppierten Index für CompanyId asc, UserId asc.

Es wird von vielen verschiedenen Threads aufgerufen, und ich erhalte ständig Deadlocks zwischen verschiedenen Prozessen, die dieselbe Anweisung aufrufen. Mein Verständnis war, dass das "with (holdlock)" notwendig war, um Fehler beim Einfügen / Aktualisieren von Rennbedingungen zu vermeiden.

Ich gehe davon aus, dass zwei verschiedene Threads Zeilen (oder Seiten) in unterschiedlicher Reihenfolge sperren, wenn sie die Einschränkungen validieren, und somit Deadlocks verursachen.

Ist das eine richtige Annahme?

Was ist der beste Weg, um diese Situation zu lösen (dh keine Deadlocks, minimale Auswirkung auf die Multi-Thread-Leistung)?

Abfrageplan-Image (Wenn Sie das Bild in einem neuen Tab anzeigen, ist es lesbar. Entschuldigen Sie die geringe Größe.)

  • Es sind höchstens 28 Zeilen in der @datatable.
  • Ich habe den Code zurückverfolgt und sehe nirgendwo, dass wir hier eine Transaktion starten.
  • Der Fremdschlüssel ist so eingerichtet, dass er nur beim Löschen kaskadiert, und es wurden keine Löschvorgänge aus der übergeordneten Tabelle ausgeführt.
Sako73
quelle

Antworten:

12

OK, nachdem ich alles ein paar Mal durchgesehen habe, denke ich, dass Ihre Grundannahme richtig war. Was hier wahrscheinlich vor sich geht, ist Folgendes:

  1. Der MATCH-Teil des MERGE prüft den Index auf Übereinstimmungen und sperrt diese Zeilen / Seiten währenddessen.

  2. Wenn es eine Zeile ohne Übereinstimmung gibt, wird versucht, zuerst die neue Indexzeile einzufügen, sodass eine Schreibsperre für die Zeile / Seite angefordert wird ...

Wenn jedoch ein anderer Benutzer in derselben Zeile / Seite ebenfalls zu Schritt 1 gelangt ist, wird der erste Benutzer für das Update gesperrt, und ...

Wenn der zweite Benutzer ebenfalls auf derselben Seite einfügen muss, befindet er sich in einem Deadlock.

AFAIK, es gibt nur einen (einfachen) Weg, um 100% ig sicher zu sein, dass Sie mit dieser Prozedur keinen Deadlock bekommen, und das wäre, einen TABLOCKX-Hinweis zur MERGE hinzuzufügen, aber das hätte wahrscheinlich einen wirklich schlechten Einfluss auf die Leistung.

Es ist möglich, dass das Hinzufügen eines TABLOCK-Hinweises ausreicht, um das Problem zu lösen, ohne dass dies einen großen Einfluss auf Ihre Leistung hat.

Schließlich können Sie auch versuchen, PAGLOCK, XLOCK oder sowohl PAGLOCK als auch XLOCK hinzuzufügen. Auch das könnte funktionieren und die Leistung könnte nicht zu schrecklich sein. Sie müssen es versuchen, um zu sehen.

RBarryYoung
quelle
Denken Sie, dass die Snapshot-Isolationsstufe (Zeilenversionierung) hier hilfreich sein könnte?
Mikael Eriksson
Vielleicht. Oder es kann die Deadlock-Ausnahmen in Nebenläufigkeitsausnahmen verwandeln.
RBarryYoung
2
Das Angeben des TABLOCK-Hinweises für eine Tabelle, die das Ziel einer INSERT-Anweisung ist, hat den gleichen Effekt wie das Angeben des TABLOCKX-Hinweises. (Quelle: msdn.microsoft.com/en-us/library/bb510625.aspx )
Dienstag,
31

Es würde kein Problem geben, wenn die Tabellenvariable immer nur einen Wert enthalten würde. Bei mehreren Zeilen gibt es eine neue Möglichkeit für Deadlocks. Angenommen, zwei gleichzeitige Prozesse (A & B) werden mit Tabellenvariablen ausgeführt, die (1, 2) und (2, 1) für dieselbe Firma enthalten.

Prozess A liest das Ziel, findet keine Zeile und fügt den Wert '1' ein. Es enthält eine exklusive Zeilensperre für den Wert '1'. Prozess B liest das Ziel, findet keine Zeile und fügt den Wert '2' ein. Es enthält eine exklusive Zeilensperre für den Wert '2'.

Jetzt muss Prozess A Zeile 2 verarbeiten, und Prozess B muss Zeile 1 verarbeiten. Keiner der Prozesse kann Fortschritte erzielen, da eine Sperre erforderlich ist, die nicht mit der exklusiven Sperre des anderen Prozesses kompatibel ist.

Um Deadlocks mit mehreren Zeilen zu vermeiden, müssen die Zeilen jedes Mal in derselben Reihenfolge verarbeitet (und auf Tabellen zugegriffen) werden . Die in der Frage gezeigte Tabellenvariable im Ausführungsplan ist ein Heap, daher haben die Zeilen keine intrinsische Reihenfolge (sie werden mit hoher Wahrscheinlichkeit in der Reihenfolge des Einfügens gelesen, obwohl dies nicht garantiert ist):

Bestehender Plan

Das Fehlen einer konsistenten Zeilenverarbeitungsreihenfolge führt direkt zur Deadlock-Möglichkeit. Eine zweite Überlegung ist, dass das Fehlen einer Schlüssel-Eindeutigkeitsgarantie bedeutet, dass eine Tischspule erforderlich ist, um einen korrekten Halloween-Schutz zu gewährleisten. Der Spool ist ein eifriger Spool, dh alle Zeilen werden in eine Tempdb-Worktable geschrieben, bevor sie zurückgelesen und für den Einfügeoperator wiedergegeben werden.

Neudefinieren der TYPEVariablen der Tabelle, um ein Cluster einzuschließen PRIMARY KEY:

DROP TYPE dbo.CoUserData;

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL PRIMARY KEY CLUSTERED,
    MyValue integer NOT NULL
);

Der Ausführungsplan zeigt jetzt einen Scan des Clustered-Index und die Eindeutigkeitsgarantie bedeutet, dass das Optimierungsprogramm den Tabellenspool sicher entfernen kann:

Mit Primärschlüssel

In Tests mit 5000 Iterationen der MERGEAnweisung in 128 Threads traten keine Deadlocks mit der gruppierten Tabellenvariablen auf. Ich möchte betonen, dass dies nur auf der Grundlage von Beobachtungen geschieht. Die gruppierte Tabellenvariable könnte ihre Zeilen auch ( technisch ) in einer Vielzahl von Reihenfolgen produzieren, aber die Chancen auf eine konsistente Reihenfolge sind sehr stark erhöht. Das beobachtete Verhalten muss natürlich für jedes neue kumulative Update, Service Pack oder jede neue Version von SQL Server erneut getestet werden.

Falls die Definition der Tabellenvariablen nicht geändert werden kann, gibt es eine andere Alternative:

MERGE dbo.CompanyUser AS R
USING 
    (SELECT DISTINCT MyKey, MyValue FROM @DataTable) AS NewData ON
    R.CompanyId = @CompanyID
    AND R.UserID = @UserID
    AND R.MyKey = NewData.MyKey
WHEN NOT MATCHED THEN 
    INSERT 
        (CompanyID, UserID, MyKey, MyValue) 
    VALUES
        (@CompanyID, @UserID, NewData.MyKey, NewData.MyValue)
OPTION (ORDER GROUP);

Dadurch wird auch die Beseitigung der Spool (und der Konsistenz der Zeilenreihenfolge) auf Kosten der Einführung einer expliziten Sortierung erreicht:

Plan sortieren

Dieser Plan erzeugte auch keine Deadlocks unter Verwendung des gleichen Tests. Reproduktionsskript unten:

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL /* PRIMARY KEY */,
    MyValue integer NOT NULL
);
GO
CREATE TABLE dbo.Company
(
    CompanyID   integer NOT NULL

    CONSTRAINT PK_Company
        PRIMARY KEY (CompanyID)
);
GO
CREATE TABLE dbo.CompanyUser
(
    CompanyID   integer NOT NULL,
    UserID      integer NOT NULL,
    MyKey       integer NOT NULL,
    MyValue     integer NOT NULL

    CONSTRAINT PK_CompanyUser
        PRIMARY KEY CLUSTERED
            (CompanyID, UserID, MyKey),

    FOREIGN KEY (CompanyID)
        REFERENCES dbo.Company (CompanyID),
);
GO
CREATE NONCLUSTERED INDEX nc1
ON dbo.CompanyUser (CompanyID, UserID);
GO
INSERT dbo.Company (CompanyID) VALUES (1);
GO
DECLARE 
    @DataTable AS dbo.CoUserData,
    @CompanyID integer = 1,
    @UserID integer = 1;

INSERT @DataTable
SELECT TOP (10)
    V.MyKey,
    V.MyValue
FROM
(
    VALUES
        (1, 1),
        (2, 2),
        (3, 3),
        (4, 4),
        (5, 5),
        (6, 6),
        (7, 7),
        (8, 8),
        (9, 9)
) AS V (MyKey, MyValue)
ORDER BY NEWID();

BEGIN TRANSACTION;

    -- Test MERGE statement here

ROLLBACK TRANSACTION;
Paul White sagt GoFundMonica
quelle
8

Ich denke, SQL_Kiwi hat eine sehr gute Analyse geliefert. Wenn Sie das Problem in der Datenbank lösen müssen, sollten Sie seinem Vorschlag folgen. Natürlich müssen Sie jedes Mal, wenn Sie ein Upgrade durchführen, ein Service Pack anwenden oder einen Index oder eine indizierte Ansicht hinzufügen / ändern, erneut testen, dass es weiterhin für Sie funktioniert.

Es gibt drei weitere Alternativen:

  1. Sie können Ihre Einfügungen serialisieren, damit sie nicht kollidieren: Sie können sp_getapplock zu Beginn Ihrer Transaktion aufrufen und eine exklusive Sperre erwerben, bevor Sie MERGE ausführen. Natürlich musst du es noch auf Stress testen.

  2. Sie können festlegen, dass ein Thread alle Ihre Einfügungen verarbeitet, sodass Ihr App-Server die Parallelität verarbeitet.

  3. Sie können es nach Deadlocks automatisch wiederholen - dies ist möglicherweise der langsamste Ansatz, wenn die Parallelität hoch ist.

In beiden Fällen können nur Sie die Auswirkung Ihrer Lösung auf die Leistung bestimmen.

Normalerweise haben wir überhaupt keine Deadlocks in unserem System, obwohl wir ein großes Potenzial dafür haben. Im Jahr 2011 ist uns bei einer Bereitstellung ein Fehler unterlaufen, und innerhalb weniger Stunden traten ein halbes Dutzend Deadlocks auf, die alle demselben Szenario folgten. Ich habe das bald behoben und das war der letzte Schrei für das Jahr.

In unserem System verwenden wir meistens Ansatz 1. Es funktioniert wirklich gut für uns.

AK
quelle
-1

Ein anderer möglicher Ansatz - Ich habe festgestellt, dass Merge manchmal Probleme mit dem Sperren und der Leistung aufwirft. Es kann sich lohnen, mit der Abfrageoption Option (MaxDop x) zu spielen

In der dunklen und fernen Vergangenheit verfügte SQL Server über die Option zum Sperren auf Zeilenebene einfügen - dies scheint jedoch ein Todesfall zu sein, jedoch sollte eine Cluster-PK mit einer Identität dazu führen, dass die Einfügungen sauber ausgeführt werden.

Ed Green
quelle