Ich habe eine Tabelle, die von einer Legacy-Anwendung als Ersatz für IDENTITY
Felder in verschiedenen anderen Tabellen verwendet wird.
In jeder Zeile der Tabelle wird die zuletzt verwendete ID LastID
fü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 IDName
Feld:
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 TRANSACTION
bis 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, LastID
kö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.
quelle
SERIALIZABLE
hierher.Antworten:
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:
Die folgenden beiden Prozeduren können sehr wahrscheinlich zu einem Deadlock führen:
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:
Führen Sie auf einer anderen Registerkarte dieses Skript aus.
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:
Verwenden einer Tabelle mit einer Zeile, um Deadlocks zu beseitigen
Anstatt sp_getapplock aufzurufen, können wir die folgende Tabelle ändern:
Sobald wir diese Tabelle erstellt und gefüllt haben, können wir die folgende Zeile ersetzen
mit diesem in beiden Verfahren:
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.
quelle
UPDATE dbo.DeadlockTestMutex SET Toggle = 1 - Toggle WHERE ID = 1;
verhindert die Verwendung Deadlocks?Die Verwendung des
XLOCK
Hinweises auf IhreSELECT
Vorgehensweise oder die folgenden HinweiseUPDATE
sollte gegen diese Art von Deadlock immun sein:Wird mit ein paar anderen Varianten zurückkehren (wenn nicht geschlagen!).
quelle
XLOCK
verhindert wird, dass ein vorhandener Leistungsindikator über mehrere Verbindungen aktualisiert wird, müssen Sie nichtTABLOCKX
verhindern, dass mehrere Verbindungen denselben neuen Leistungsindikator hinzufügen?Mike Defehr hat mir einen eleganten Weg gezeigt, um dies auf sehr einfache Weise zu erreichen:
(Der Vollständigkeit halber ist hier die Tabelle, die dem gespeicherten Prozess zugeordnet ist.)
Dies ist der Ausführungsplan für die neueste Version:
Und das ist der Ausführungsplan für die Originalversion (Deadlock anfällig):
Klar, die neue Version gewinnt!
Zum Vergleich
(XLOCK)
ergibt die Zwischenversion mit dem etc folgenden Plan:Ich würde sagen, das ist ein Gewinn! Vielen Dank für die Hilfe aller!
quelle
SERIALIZABLE
existiert 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 .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 ...
quelle
Ich habe im letzten Jahr einen ähnlichen Deadlock in einem System behoben, indem ich Folgendes geändert habe:
Dazu:
Im Allgemeinen ist die Auswahl einer
COUNT
Option 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 verwendenIF NOT EXISTS
stattIF COUNT() = 0
) und (b) Der zusätzliche Scan ist völlig unnötig. DerUPDATE
führt im Wesentlichen die gleiche Prüfung durch.Außerdem sieht das für mich nach einem ernsthaften Codegeruch aus:
Worum geht es hier? Warum nicht einfach eine Identitätsspalte verwenden oder diese Sequenz
ROW_NUMBER()
zur Abfragezeit ableiten ?quelle
IDENTITY
. Diese Tabelle unterstützt in MS Access geschriebenen Legacy-Code, der für eine Nachrüstung mit erheblichem Aufwand verbunden wäre. DieSET @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önnteROW_NUMBER()
?LastID
in Ihrem Modell wirklich bedeutet. Was ist seine Aufgabe? Der Name ist nicht gerade selbsterklärend. Wie wird es von Access verwendet?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.