Eu tenho uma tabela que é usada por um aplicativo legado como substituto de IDENTITY
campos em várias outras tabelas.
Cada linha na tabela armazena o último ID usado LastID
para o campo nomeado em IDName
.
Ocasionalmente, o proc armazenado obtém um impasse - acredito que construí um manipulador de erros apropriado; no entanto, estou interessado em ver se esta metodologia funciona como eu acho que funciona, ou se estou latindo para a árvore errada aqui.
Estou bastante certo de que deve haver uma maneira de acessar esta tabela sem nenhum impasse.
O próprio banco de dados é configurado com READ_COMMITTED_SNAPSHOT = 1
.
Primeiro, aqui está a tabela:
CREATE TABLE [dbo].[tblIDs](
[IDListID] [int] NOT NULL
CONSTRAINT PK_tblIDs
PRIMARY KEY CLUSTERED
IDENTITY(1,1) ,
[IDName] [nvarchar](255) NULL,
[LastID] [int] NULL,
);
E o índice não clusterizado no IDName
campo:
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
Alguns dados de exemplo:
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeTestID', 1);
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeOtherTestID', 1);
GO
O procedimento armazenado usado para atualizar os valores armazenados na tabela e retornar o próximo ID:
CREATE PROCEDURE [dbo].[GetNextID](
@IDName nvarchar(255)
)
AS
BEGIN
/*
Description: Increments and returns the LastID value from tblIDs
for a given IDName
Author: Hannah 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
Exemplos de execuções do proc armazenado:
EXEC GetNextID 'SomeTestID';
NewID
2
EXEC GetNextID 'SomeTestID';
NewID
3
EXEC GetNextID 'SomeOtherTestID';
NewID
2
EDITAR:
Adicionei um novo índice, pois o índice existente IX_tblIDs_Name não está sendo usado pelo SP; Suponho que o processador de consulta esteja usando o índice clusterizado, pois precisa do valor armazenado em LastID. De qualquer forma, este índice é usado pelo plano de execução real:
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);
EDIÇÃO #2:
Eu segui o conselho que @AaronBertrand deu e modifiquei um pouco. A ideia geral aqui é refinar a declaração para eliminar bloqueios desnecessários e, em geral, tornar o SP mais eficiente.
O código abaixo substitui o código acima de BEGIN TRANSACTION
to 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;
Como nosso código nunca adiciona um registro a esta tabela com 0 LastID
, podemos supor que, se @NewID for 1, a intenção é anexar um novo ID à lista, caso contrário, estamos atualizando uma linha existente na lista.
First, I would avoid making a round trip to the database for every value. For example, if your application knows it needs 20 new IDs, do not make 20 round trips. Make only one stored procedure call, and increment the counter by 20. Also it might be better to split your table into multiple ones.
It is possible to avoid deadlocks altogether. I have no deadlocks at all in my system. There are several ways to accomplish that. I will show how I would use sp_getapplock to eliminate deadlocks. I have no idea if this will work for you, because SQL Server is closed source, so I cannot see the source code, and as such I do not know if I have tested all possible cases.
The following describes what works for me. YMMV.
First, let us start with a scenario where we always get a considerable amount of deadlocks. Second, we shall use sp_getapplock eliminate them. The most important point here is to stress test your solution. Your solution may be different, but you need to expose it to high concurrency, as I will demonstrate later.
Prerequisites
Let us set up a table with some test data:
The following two procedures are quite likely to embrace in a deadlock:
Reproducing deadlocks
The following loops should reproduce more than 20 deadlocks every time you run them. If you get less than 20, increase the number of iterations.
In one tab, run this;
In another tab, run this script.
Make sure you start both within a couple of seconds.
Using sp_getapplock to eliminate deadlocks
Alter both procedures, rerun the loop, and see that you no longer have deadlocks:
Using a table with one row to eliminate deadlocks
Instead of invoking sp_getapplock, we can modify the following table:
Once we have this table created and populated, we can replace the following line
with this one, in both procedures:
You can rerun the stress test, and see for yourself that we have no deadlocks.
Conclusion
As we have seen, sp_getapplock can be used to serialize access to other resources. As such it can be used to eliminate deadlocks.
Of course, this can significantly slow down modifications. To address that, we need to choose the right granularity for the exclusive lock, and whenever possible, work with sets instead of individual rows.
Antes de usar essa abordagem, você precisa fazer um teste de estresse você mesmo. Primeiro, você precisa ter certeza de obter pelo menos algumas dúzias de impasses com sua abordagem original. Segundo, você não deve obter deadlocks ao executar novamente o mesmo script de reprodução usando o procedimento armazenado modificado.
Em geral, não acho que haja uma boa maneira de determinar se seu T-SQL está protegido contra deadlocks apenas observando-o ou observando o plano de execução. IMO a única maneira de determinar se o seu código é propenso a deadlocks é expô-lo a alta simultaneidade.
Boa sorte com a eliminação de impasses! Não temos nenhum impasse em nosso sistema, o que é ótimo para nosso equilíbrio entre vida profissional e pessoal.
O uso da
XLOCK
dica em suaSELECT
abordagem ou no seguinteUPDATE
deve ser imune a esse tipo de impasse:Voltará com algumas outras variantes (se não for derrotado!).
Mike Defehr me mostrou uma maneira elegante de fazer isso de uma maneira muito leve:
(Para completar, aqui está a tabela associada ao proc armazenado)
Este é o plano de execução para a versão mais recente:
E este é o plano de execução para a versão original (suscetível a deadlock):
Claramente, a nova versão ganha!
Para efeito de comparação, a versão intermediária com o
(XLOCK)
etc, produz o seguinte plano:Eu diria que é uma vitória! Obrigado pela ajuda de todos!
Not to steal Mark Storey-Smith's thunder, but he is onto something with his post above (that has incidentally received the most upvotes). The advice I gave Hannah was centered around the "UPDATE set @variable = column = column + value" construct which I find really cool, but I think may be undocumented (it has to be supported, though because it is there specifically for the TCP benchmarks).
Here is a variation of Mark's answer - because you are returning the new ID value as a recordset, you can do away with the scalar variable entirely, no explicit transaction should be necessary either, and I would agree that messing around with isolation levels is unnecessary as well. The result is very clean and pretty slick...
Corrigi um impasse semelhante em um sistema no ano passado, alterando isso:
Para isso:
Em geral, selecionar um
COUNT
apenas para determinar presença ou ausência é um desperdício. Nesse caso, como é 0 ou 1, não é como se fosse muito trabalho, mas (a) esse hábito pode sangrar em outros casos em que será muito mais caro (nesses casos, useIF NOT EXISTS
em vez deIF COUNT() = 0
), e (b) a varredura adicional é completamente desnecessária. OUPDATE
executa essencialmente a mesma verificação.Além disso, isso parece um cheiro de código sério para mim:
Qual é o ponto aqui? Por que não usar apenas uma coluna de identidade ou derivar essa sequência usando
ROW_NUMBER()
no momento da consulta?