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.
SERIALIZABLE
hierher.