Behandeln des gleichzeitigen Zugriffs auf eine Schlüsseltabelle ohne Deadlocks in SQL Server

32

Ich habe eine Tabelle, die von einer Legacy-Anwendung als Ersatz für IDENTITYFelder in verschiedenen anderen Tabellen verwendet wird.

In jeder Zeile der Tabelle wird die zuletzt verwendete ID LastIDfür das in angegebene Feld gespeichert IDName.

Gelegentlich kommt es zu einem Deadlock des gespeicherten Prozesses. Ich glaube, ich habe einen geeigneten Fehlerbehandler erstellt. Ich bin jedoch interessiert zu sehen, ob diese Methode so funktioniert, wie ich denke, oder ob ich hier den falschen Baum anklopfe.

Ich bin mir ziemlich sicher, dass es eine Möglichkeit geben sollte, ohne Deadlocks auf diesen Tisch zuzugreifen.

Die Datenbank selbst wird mit konfiguriert READ_COMMITTED_SNAPSHOT = 1.

Hier ist zunächst die Tabelle:

CREATE TABLE [dbo].[tblIDs](
    [IDListID] [int] NOT NULL 
        CONSTRAINT PK_tblIDs 
        PRIMARY KEY CLUSTERED 
        IDENTITY(1,1) ,
    [IDName] [nvarchar](255) NULL,
    [LastID] [int] NULL,
);

Und der nicht gruppierte Index für das IDNameFeld:

CREATE NONCLUSTERED INDEX [IX_tblIDs_IDName] 
ON [dbo].[tblIDs]
(
    [IDName] ASC
) 
WITH (
    PAD_INDEX = OFF
    , STATISTICS_NORECOMPUTE = OFF
    , SORT_IN_TEMPDB = OFF
    , DROP_EXISTING = OFF
    , ONLINE = OFF
    , ALLOW_ROW_LOCKS = ON
    , ALLOW_PAGE_LOCKS = ON
    , FILLFACTOR = 80
);

GO

Einige Beispieldaten:

INSERT INTO tblIDs (IDName, LastID) 
    VALUES ('SomeTestID', 1);
INSERT INTO tblIDs (IDName, LastID) 
    VALUES ('SomeOtherTestID', 1);
GO

Die gespeicherte Prozedur, mit der die in der Tabelle gespeicherten Werte aktualisiert und die nächste ID zurückgegeben werden:

CREATE PROCEDURE [dbo].[GetNextID](
    @IDName nvarchar(255)
)
AS
BEGIN
    /*
        Description:    Increments and returns the LastID value from tblIDs
        for a given IDName
        Author:         Max Vernon
        Date:           2012-07-19
    */

    DECLARE @Retry int;
    DECLARE @EN int, @ES int, @ET int;
    SET @Retry = 5;
    DECLARE @NewID int;
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    SET NOCOUNT ON;
    WHILE @Retry > 0
    BEGIN
        BEGIN TRY
            BEGIN TRANSACTION;
            SET @NewID = COALESCE((SELECT LastID 
                FROM tblIDs 
                WHERE IDName = @IDName),0)+1;
            IF (SELECT COUNT(IDName) 
                FROM tblIDs 
                WHERE IDName = @IDName) = 0 
                    INSERT INTO tblIDs (IDName, LastID) 
                    VALUES (@IDName, @NewID)
            ELSE
                UPDATE tblIDs 
                SET LastID = @NewID 
                WHERE IDName = @IDName;
            COMMIT TRANSACTION;
            SET @Retry = -2; /* no need to retry since the operation completed */
        END TRY
        BEGIN CATCH
            IF (ERROR_NUMBER() = 1205) /* DEADLOCK */
                SET @Retry = @Retry - 1;
            ELSE
                BEGIN
                SET @Retry = -1;
                SET @EN = ERROR_NUMBER();
                SET @ES = ERROR_SEVERITY();
                SET @ET = ERROR_STATE()
                RAISERROR (@EN,@ES,@ET);
                END
            ROLLBACK TRANSACTION;
        END CATCH
    END
    IF @Retry = 0 /* must have deadlock'd 5 times. */
    BEGIN
        SET @EN = 1205;
        SET @ES = 13;
        SET @ET = 1
        RAISERROR (@EN,@ES,@ET);
    END
    ELSE
        SELECT @NewID AS NewID;
END
GO

Beispielausführungen des gespeicherten Prozesses:

EXEC GetNextID 'SomeTestID';

NewID
2

EXEC GetNextID 'SomeTestID';

NewID
3

EXEC GetNextID 'SomeOtherTestID';

NewID
2

BEARBEITEN:

Ich habe einen neuen Index hinzugefügt, da der vorhandene Index IX_tblIDs_Name nicht vom SP verwendet wird. Ich gehe davon aus, dass der Abfrageprozessor den Clustered-Index verwendet, da er den in LastID gespeicherten Wert benötigt. Auf jeden Fall wird dieser Index vom tatsächlichen Ausführungsplan verwendet:

CREATE NONCLUSTERED INDEX IX_tblIDs_IDName_LastID 
ON dbo.tblIDs
(
    IDName ASC
) 
INCLUDE
(
    LastID
)
WITH (FILLFACTOR = 100
    , ONLINE=ON
    , ALLOW_ROW_LOCKS = ON
    , ALLOW_PAGE_LOCKS = ON);

EDIT # 2:

Ich habe den Rat von @AaronBertrand angenommen und ihn leicht modifiziert. Die allgemeine Idee dabei ist, die Anweisung zu verfeinern, um unnötige Sperren zu beseitigen und den SP insgesamt effizienter zu gestalten.

Der folgende Code ersetzt den obigen Code von BEGIN TRANSACTIONbis END TRANSACTION:

BEGIN TRANSACTION;
SET @NewID = COALESCE((SELECT LastID 
        FROM dbo.tblIDs 
        WHERE IDName = @IDName), 0) + 1;

IF @NewID = 1
    INSERT INTO tblIDs (IDName, LastID) 
    VALUES (@IDName, @NewID);
ELSE
    UPDATE dbo.tblIDs 
    SET LastID = @NewID 
    WHERE IDName = @IDName;

COMMIT TRANSACTION;

Da unser Code dieser Tabelle niemals einen Datensatz mit 0 hinzufügt, LastIDkönnen wir davon ausgehen, dass bei @NewID 1 eine neue ID an die Liste angehängt wird, andernfalls wird eine vorhandene Zeile in der Liste aktualisiert.

Max Vernon
quelle
Wie Sie die Datenbank für die Unterstützung von RCSI konfiguriert haben, spielt keine Rolle. Sie eskalieren absichtlich SERIALIZABLEhierher.
Aaron Bertrand
Ja, ich wollte nur alle relevanten Informationen hinzufügen. Ich bin froh, dass Sie bestätigen, dass es irrelevant ist!
Max Vernon
Es ist sehr einfach, sp_getapplock zum Deadlock-Opfer zu machen, aber nicht, wenn Sie mit der Transaktion beginnen, rufen Sie sp_getapplock einmal auf, um eine exklusive Sperre zu erhalten, und fahren Sie mit Ihrer Änderung fort.
AK
1
Ist IDName eindeutig? Dann empfehlen wir " einzigartigen nicht gruppierten Index erstellen ". Wenn Sie jedoch Nullwerte benötigen, muss auch der Index gefiltert werden .
Crokusek

Antworten:

15

Erstens würde ich es vermeiden, für jeden Wert einen Roundtrip zur Datenbank durchzuführen. Wenn Ihre Anwendung beispielsweise weiß, dass 20 neue IDs erforderlich sind, sollten Sie keine 20 Roundtrips durchführen. Nehmen Sie nur einen Aufruf einer gespeicherten Prozedur vor und erhöhen Sie den Zähler um 20. Es ist möglicherweise auch besser, Ihre Tabelle in mehrere zu teilen.

Deadlocks lassen sich ganz vermeiden. Ich habe überhaupt keine Deadlocks in meinem System. Dafür gibt es verschiedene Möglichkeiten. Ich werde zeigen, wie ich sp_getapplock verwenden würde, um Deadlocks zu beseitigen. Ich habe keine Ahnung, ob dies für Sie funktionieren wird, da SQL Server als geschlossener Quellcode ausgeführt wird. Daher kann ich den Quellcode nicht anzeigen. Daher weiß ich nicht, ob ich alle möglichen Fälle getestet habe.

Im Folgenden wird beschrieben, was bei mir funktioniert. YMMV.

Beginnen wir mit einem Szenario, in dem wir immer eine beträchtliche Anzahl von Deadlocks erhalten. Zweitens werden wir sp_getapplock verwenden, um sie zu beseitigen. Der wichtigste Punkt hierbei ist der Stresstest Ihrer Lösung. Ihre Lösung mag unterschiedlich sein, aber Sie müssen sie einer hohen Parallelität aussetzen, wie ich später demonstrieren werde.

Voraussetzungen

Lassen Sie uns eine Tabelle mit einigen Testdaten erstellen:

CREATE TABLE dbo.Numbers(n INT NOT NULL PRIMARY KEY); 
GO 

INSERT INTO dbo.Numbers 
    ( n ) 
        VALUES  ( 1 ); 
GO 
DECLARE @i INT; 
    SET @i=0; 
WHILE @i<21  
    BEGIN 
    INSERT INTO dbo.Numbers 
        ( n ) 
        SELECT n + POWER(2, @i) 
        FROM dbo.Numbers; 
    SET @i = @i + 1; 
    END;  
GO

SELECT n AS ID, n AS Key1, n AS Key2, 0 AS Counter1, 0 AS Counter2
INTO dbo.DeadlockTest FROM dbo.Numbers
GO

ALTER TABLE dbo.DeadlockTest ADD CONSTRAINT PK_DeadlockTest PRIMARY KEY(ID);
GO

CREATE INDEX DeadlockTestKey1 ON dbo.DeadlockTest(Key1);
GO

CREATE INDEX DeadlockTestKey2 ON dbo.DeadlockTest(Key2);
GO

Die folgenden beiden Prozeduren können sehr wahrscheinlich zu einem Deadlock führen:

CREATE PROCEDURE dbo.UpdateCounter1 @Key1 INT
AS
SET NOCOUNT ON ;
SET XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ;
UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
SET @Key1=@Key1-10000;
UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
COMMIT;
GO

CREATE PROCEDURE dbo.UpdateCounter2 @Key2 INT
AS
SET NOCOUNT ON ;
SET XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ;
SET @Key2=@Key2-10000;
UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
SET @Key2=@Key2+10000;
UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
COMMIT;
GO

Deadlocks reproduzieren

Die folgenden Schleifen sollten jedes Mal, wenn Sie sie ausführen, mehr als 20 Deadlocks reproduzieren. Wenn Sie weniger als 20 erhalten, erhöhen Sie die Anzahl der Iterationen.

Führen Sie auf einer Registerkarte Folgendes aus:

DECLARE @i INT, @DeadlockCount INT;
SELECT @i=0, @DeadlockCount=0;

WHILE @i<5000 BEGIN ;
  BEGIN TRY 
    EXEC dbo.UpdateCounter1 @Key1=123456;
  END TRY
  BEGIN CATCH
    SET @DeadlockCount = @DeadlockCount + 1;
    ROLLBACK;
  END CATCH ;
  SET @i = @i + 1;
END;
SELECT 'Deadlocks caught: ', @DeadlockCount ;

Führen Sie auf einer anderen Registerkarte dieses Skript aus.

DECLARE @i INT, @DeadlockCount INT;
SELECT @i=0, @DeadlockCount=0;

WHILE @i<5000 BEGIN ;
  BEGIN TRY 
    EXEC dbo.UpdateCounter2 @Key2=123456;
  END TRY
  BEGIN CATCH
    SET @DeadlockCount = @DeadlockCount + 1;
    ROLLBACK;
  END CATCH ;
  SET @i = @i + 1;
END;
SELECT 'Deadlocks caught: ', @DeadlockCount ;

Stellen Sie sicher, dass Sie beide innerhalb weniger Sekunden starten.

Verwenden von sp_getapplock, um Deadlocks zu beseitigen

Ändern Sie beide Prozeduren, führen Sie die Schleife erneut aus und stellen Sie sicher, dass Sie keine Deadlocks mehr haben:

ALTER PROCEDURE dbo.UpdateCounter1 @Key1 INT
AS
SET NOCOUNT ON ;
SET XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ;
EXEC sp_getapplock @Resource='DeadlockTest', @LockMode='Exclusive';
UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
SET @Key1=@Key1-10000;
UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
COMMIT;
GO

ALTER PROCEDURE dbo.UpdateCounter2 @Key2 INT
AS
SET NOCOUNT ON ;
SET XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ;
EXEC sp_getapplock @Resource='DeadlockTest', @LockMode='Exclusive';
SET @Key2=@Key2-10000;
UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
SET @Key2=@Key2+10000;
UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
COMMIT;
GO

Verwenden einer Tabelle mit einer Zeile, um Deadlocks zu beseitigen

Anstatt sp_getapplock aufzurufen, können wir die folgende Tabelle ändern:

CREATE TABLE dbo.DeadlockTestMutex(
ID INT NOT NULL,
CONSTRAINT PK_DeadlockTestMutex PRIMARY KEY(ID),
Toggle INT NOT NULL);
GO

INSERT INTO dbo.DeadlockTestMutex(ID, Toggle)
VALUES(1,0);

Sobald wir diese Tabelle erstellt und gefüllt haben, können wir die folgende Zeile ersetzen

EXEC sp_getapplock @Resource='DeadlockTest', @LockMode='Exclusive';

mit diesem in beiden Verfahren:

UPDATE dbo.DeadlockTestMutex SET Toggle = 1 - Toggle WHERE ID = 1;

Sie können den Stresstest wiederholen und selbst feststellen, dass wir keine Deadlocks haben.

Fazit

Wie wir gesehen haben, kann sp_getapplock verwendet werden, um den Zugriff auf andere Ressourcen zu serialisieren. Als solches kann es verwendet werden, um Deadlocks zu beseitigen.

Dies kann natürlich Änderungen erheblich verlangsamen. Um dies zu beheben, müssen wir die richtige Granularität für die exklusive Sperre auswählen und, wann immer möglich, mit Mengen statt mit einzelnen Zeilen arbeiten.

Bevor Sie diesen Ansatz verwenden, müssen Sie ihn selbst einem Stresstest unterziehen. Zunächst müssen Sie sicherstellen, dass Sie mit Ihrem ursprünglichen Ansatz mindestens ein paar Dutzend Deadlocks erzielen. Zweitens sollten Sie keine Deadlocks erhalten, wenn Sie dasselbe Reproskript mit einer geänderten gespeicherten Prozedur erneut ausführen.

Im Allgemeinen glaube ich nicht, dass es eine gute Möglichkeit gibt, festzustellen, ob Ihr T-SQL vor Deadlocks sicher ist, indem Sie es oder den Ausführungsplan betrachten. Die einzige Möglichkeit, festzustellen, ob Ihr Code für Deadlocks anfällig ist, besteht darin, ihn einer hohen Parallelität auszusetzen.

Viel Glück beim Beseitigen von Deadlocks! Wir haben überhaupt keine Deadlocks in unserem System, was sich positiv auf unsere Work-Life-Balance auswirkt.

AK
quelle
2
+1 als sp_getapplock ist ein nützliches Werkzeug, das nicht gut bekannt ist. Angesichts eines furchtbaren Durcheinanders, dessen Beseitigung möglicherweise einige Zeit in Anspruch nimmt, ist es ein praktischer Trick, einen festgefahrenen Prozess zu serialisieren. Aber sollte es für einen Fall wie diesen die erste Wahl sein, der leicht zu verstehen ist und mit Standard-Schließmechanismen behandelt werden kann (vielleicht sollte)?
Mark Storey-Smith
2
@ MarkStorey-Smith Es ist meine erste Wahl, da ich es nur einmal recherchiert und getestet habe und es in jeder Situation wiederverwenden kann - die Serialisierung ist bereits erfolgt, sodass alles, was nach sp_getapplock passiert, keinen Einfluss auf das Ergebnis hat. Mit Standard-Sperrmechanismen kann ich nie so sicher sein - das Hinzufügen eines Index oder das Abrufen eines anderen Ausführungsplans kann zu Deadlocks führen, wo zuvor keine vorhanden waren. Frag mich, woher ich das weiß.
AK
Ich vermisse etwas Offensichtliches, aber wie UPDATE dbo.DeadlockTestMutex SET Toggle = 1 - Toggle WHERE ID = 1;verhindert die Verwendung Deadlocks?
Dale K,
9

Die Verwendung des XLOCKHinweises auf Ihre SELECTVorgehensweise oder die folgenden Hinweise UPDATEsollte gegen diese Art von Deadlock immun sein:

DECLARE @Output TABLE ([NewId] INT);
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

BEGIN TRANSACTION;

UPDATE
    dbo.tblIDs WITH (XLOCK)
SET 
    LastID = LastID + 1
OUTPUT
    INSERTED.[LastId] INTO @Output
WHERE
    IDName = @IDName;

IF(@@ROWCOUNT = 1)
BEGIN
    SELECT @NewId = [NewId] FROM @Output;
END
ELSE
BEGIN
    SET @NewId = 1;

    INSERT dbo.tblIDs
        (IDName, LastID)
    VALUES
        (@IDName, @NewId);
END

SELECT [NewId] = @NewId ;

COMMIT TRANSACTION;

Wird mit ein paar anderen Varianten zurückkehren (wenn nicht geschlagen!).

Mark Storey-Smith
quelle
Wenngleich XLOCKverhindert wird, dass ein vorhandener Leistungsindikator über mehrere Verbindungen aktualisiert wird, müssen Sie nicht TABLOCKXverhindern, dass mehrere Verbindungen denselben neuen Leistungsindikator hinzufügen?
Dale K
1
@DaleBurrell Nein, IDName ist PK oder einer eindeutigen Einschränkung unterworfen.
Mark Storey-Smith
7

Mike Defehr hat mir einen eleganten Weg gezeigt, um dies auf sehr einfache Weise zu erreichen:

ALTER PROCEDURE [dbo].[GetNextID](
    @IDName nvarchar(255)
)
AS
BEGIN
    /*
        Description:    Increments and returns the LastID value from tblIDs for a given IDName
        Author:         Max Vernon / Mike Defehr
        Date:           2012-07-19

    */

    DECLARE @Retry int;
    DECLARE @EN int, @ES int, @ET int;
    SET @Retry = 5;
    DECLARE @NewID int;
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    SET NOCOUNT ON;
    WHILE @Retry > 0
    BEGIN
        BEGIN TRY
            UPDATE dbo.tblIDs 
            SET @NewID = LastID = LastID + 1 
            WHERE IDName = @IDName;

            IF @NewID IS NULL
            BEGIN
                SET @NewID = 1;
                INSERT INTO tblIDs (IDName, LastID) VALUES (@IDName, @NewID);
            END
            SET @Retry = -2; /* no need to retry since the operation completed */
        END TRY
        BEGIN CATCH
            IF (ERROR_NUMBER() = 1205) /* DEADLOCK */
                SET @Retry = @Retry - 1;
            ELSE
                BEGIN
                SET @Retry = -1;
                SET @EN = ERROR_NUMBER();
                SET @ES = ERROR_SEVERITY();
                SET @ET = ERROR_STATE()
                RAISERROR (@EN,@ES,@ET);
                END
        END CATCH
    END
    IF @Retry = 0 /* must have deadlock'd 5 times. */
    BEGIN
        SET @EN = 1205;
        SET @ES = 13;
        SET @ET = 1
        RAISERROR (@EN,@ES,@ET);
    END
    ELSE
        SELECT @NewID AS NewID;
END
GO

(Der Vollständigkeit halber ist hier die Tabelle, die dem gespeicherten Prozess zugeordnet ist.)

CREATE TABLE [dbo].[tblIDs]
(
    IDName nvarchar(255) NOT NULL,
    LastID int NULL,
    CONSTRAINT [PK_tblIDs] PRIMARY KEY CLUSTERED 
    (
        [IDName] ASC
    ) WITH 
    (
        PAD_INDEX = OFF
        , STATISTICS_NORECOMPUTE = OFF
        , IGNORE_DUP_KEY = OFF
        , ALLOW_ROW_LOCKS = ON
        , ALLOW_PAGE_LOCKS = ON
        , FILLFACTOR = 100
    ) 
);
GO

Dies ist der Ausführungsplan für die neueste Version:

Bildbeschreibung hier eingeben

Und das ist der Ausführungsplan für die Originalversion (Deadlock anfällig):

Bildbeschreibung hier eingeben

Klar, die neue Version gewinnt!

Zum Vergleich (XLOCK)ergibt die Zwischenversion mit dem etc folgenden Plan:

Bildbeschreibung hier eingeben

Ich würde sagen, das ist ein Gewinn! Vielen Dank für die Hilfe aller!

Max Vernon
quelle
2
Sollte in der Tat funktionieren, aber Sie verwenden SERIALIZABLE, wo es nicht anwendbar ist. Hier können keine Phantomzeilen vorhanden sein. Warum sollten Sie eine vorhandene Isolationsstufe verwenden, um sie zu verhindern? Wenn jemand Ihre Prozedur von einer anderen oder von einer Verbindung aus aufruft, bei der eine äußere Transaktion gestartet wurde, werden alle weiteren Aktionen, die er initiiert, bei SERIALIZABLE fortgesetzt. Das kann chaotisch werden.
Mark Storey-Smith
2
SERIALIZABLEexistiert nicht, um Phantome zu verhindern. Es besteht die Möglichkeit, eine serialisierbare Isolationssemantik bereitzustellen , dh dieselbe dauerhafte Auswirkung auf die Datenbank, als ob die betreffenden Transaktionen in einer nicht festgelegten Reihenfolge seriell ausgeführt worden wären .
Paul White sagt GoFundMonica
6

Nicht um Mark Storey-Smiths Donner zu stehlen, aber er ist auf etwas mit seinem Posten oben (der übrigens die meisten positiven Stimmen erhalten hat). Der Rat, den ich Max gab, konzentrierte sich auf das Konstrukt "UPDATE set @variable = column = column + value", das ich wirklich cool finde, aber ich denke, es ist möglicherweise undokumentiert (es muss unterstützt werden, da es speziell für TCP ist) Benchmarks).

Hier ist eine Variation von Marks Antwort: Da Sie den neuen ID-Wert als Recordset zurückgeben, können Sie die skalare Variable vollständig entfernen. Es sollte auch keine explizite Transaktion erforderlich sein, und ich stimme zu, dass das Herumspielen mit Isolationsstufen unnötig ist auch. Das Ergebnis ist sehr sauber und ziemlich glatt ...

ALTER PROC [dbo].[GetNextID]
  @IDName nvarchar(255)
  AS
BEGIN
SET NOCOUNT ON;

DECLARE @Output TABLE ([NewID] INT);

UPDATE dbo.tblIDs SET LastID = LastID + 1
OUTPUT inserted.[LastId] INTO @Output
WHERE IDName = @IDName;

IF(@@ROWCOUNT = 1)
    SELECT [NewID] FROM @Output;
ELSE
    INSERT dbo.tblIDs (IDName, LastID)
    OUTPUT INSERTED.LastID AS [NewID]
    VALUES (@IDName,1);
END
Mike DeFehr
quelle
3
Einverstanden, dass dies immun gegen Deadlock sein sollte, aber es ist anfällig für eine Race Condition auf dem Insert, wenn Sie die Transaktion auslassen.
Mark Storey-Smith
4

Ich habe im letzten Jahr einen ähnlichen Deadlock in einem System behoben, indem ich Folgendes geändert habe:

IF (SELECT COUNT(IDName) FROM tblIDs WHERE IDName = @IDName) = 0 
  INSERT INTO tblIDs (IDName, LastID) VALUES (@IDName, @NewID)
ELSE
  UPDATE tblIDs SET LastID = @NewID WHERE IDName = @IDName;

Dazu:

UPDATE tblIDs SET LastID = @NewID WHERE IDName = @IDName;
IF @@ROWCOUNT = 0
BEGIN
  INSERT ...
END

Im Allgemeinen ist die Auswahl einer COUNTOption zur Feststellung des Vorhandenseins oder Fehlens ziemlich verschwenderisch. In diesem Fall , da es entweder 0 oder 1 ist es nicht , wie es eine Menge Arbeit ist, aber (a) kann diese Gewohnheit in anderen Fällen bluten , wo es wird viel teurer sein (in diesen Fällen verwenden IF NOT EXISTSstatt IF COUNT() = 0) und (b) Der zusätzliche Scan ist völlig unnötig. Der UPDATEführt im Wesentlichen die gleiche Prüfung durch.

Außerdem sieht das für mich nach einem ernsthaften Codegeruch aus:

SET @NewID = COALESCE((SELECT LastID FROM tblIDs WHERE IDName = @IDName),0)+1;

Worum geht es hier? Warum nicht einfach eine Identitätsspalte verwenden oder diese Sequenz ROW_NUMBER()zur Abfragezeit ableiten ?

Aaron Bertrand
quelle
Die meisten Tabellen verwenden eine IDENTITY. Diese Tabelle unterstützt in MS Access geschriebenen Legacy-Code, der für eine Nachrüstung mit erheblichem Aufwand verbunden wäre. Die SET @NewID=Zeile erhöht einfach den in der Tabelle gespeicherten Wert für die angegebene ID (aber das wissen Sie bereits). Können Sie näher erläutern, wie ich es gebrauchen könnte ROW_NUMBER()?
Max Vernon
@MaxVernon nicht ohne zu wissen, was LastIDin Ihrem Modell wirklich bedeutet. Was ist seine Aufgabe? Der Name ist nicht gerade selbsterklärend. Wie wird es von Access verwendet?
Aaron Bertrand
Eine Funktion in Access möchte einer beliebigen Tabelle, die keine IDENTITÄT hat, eine Zeile hinzufügen. First Access ruft GetNextID('WhatevertheIDFieldIsCalled')auf, um die nächste zu verwendende ID abzurufen, und fügt sie zusammen mit den erforderlichen Daten in die neue Zeile ein.
Max Vernon
Ich werde Ihre Änderung umsetzen. Ein reiner Fall von "weniger ist mehr"!
Max Vernon
1
Ihr fester Deadlock kann erneut auftreten. Ihr zweites Muster ist ebenfalls anfällig: sqlblog.com/blogs/alexander_kuznetsov/archive/2010/01/12/… Um Deadlocks zu beseitigen, würde ich sp_getapplock verwenden. Mai Mixed-Load-System mit Hunderten von Benutzern hat keine Deadlocks.
AK