Eu tenho uma tabela que é usada por um aplicativo herdado como um substituto para IDENTITY
campos em várias outras tabelas.
Cada linha na tabela armazena o último ID utilizado LastID
para o campo com o nome no IDName
.
Ocasionalmente, o processo armazenado recebe um impasse - acredito que criei um manipulador de erros apropriado; no entanto, estou interessado em ver se essa metodologia funciona da maneira que acho que funciona ou se estou latindo na árvore errada aqui.
Estou bastante certo de que deve haver uma maneira de acessar esta tabela sem nenhum conflito.
O próprio banco de dados está 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 amostra:
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: 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
Exemplo de execuções do processo armazenado:
EXEC GetNextID 'SomeTestID';
NewID
2
EXEC GetNextID 'SomeTestID';
NewID
3
EXEC GetNextID 'SomeOtherTestID';
NewID
2
EDITAR:
Eu adicionei um novo índice, pois o índice existente IX_tblIDs_Name não está sendo usado pelo SP; Presumo que o processador de consultas esteja usando o índice clusterizado, pois precisa do valor armazenado no LastID. De qualquer forma, esse í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);
EDIT # 2:
Aceitei o conselho que @AaronBertrand deu e modifiquei-o levemente. A idéia 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
para 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 assumir que se @NewID for 1, a intenção é acrescentar um novo ID à lista; caso contrário, estamos atualizando uma linha existente na lista.
fonte
SERIALIZABLE
aqui.Respostas:
Primeiro, eu evitaria fazer uma ida e volta ao banco de dados para todos os valores. Por exemplo, se seu aplicativo souber que precisa de 20 novos IDs, não faça 20 viagens de ida e volta. Faça apenas uma chamada de procedimento armazenado e aumente o contador em 20. Também pode ser melhor dividir sua tabela em várias.
É possível evitar completamente os impasses. Não tenho nenhum impasse no meu sistema. Existem várias maneiras de conseguir isso. Vou mostrar como eu usaria sp_getapplock para eliminar conflitos. Não tenho idéia se isso funcionará para você, porque o SQL Server é de código fechado, portanto não consigo ver o código-fonte e, como tal, não sei se testei todos os casos possíveis.
A seguir, descreve o que funciona para mim. YMMV.
Primeiro, vamos começar com um cenário em que sempre obtemos uma quantidade considerável de impasses. Segundo, usaremos sp_getapplock para eliminá-los. O ponto mais importante aqui é o teste de estresse de sua solução. Sua solução pode ser diferente, mas você precisa expô-la a alta simultaneidade, como demonstrarei mais adiante.
Pré-requisitos
Vamos configurar uma tabela com alguns dados de teste:
É provável que os dois procedimentos a seguir adotem um impasse:
Reprodução de deadlocks
Os loops a seguir devem reproduzir mais de 20 bloqueios toda vez que você os executar. Se você obtiver menos de 20, aumente o número de iterações.
Em uma guia, execute isso;
Em outra guia, execute este script.
Certifique-se de iniciar os dois em alguns segundos.
Usando sp_getapplock para eliminar deadlocks
Altere os dois procedimentos, execute novamente o loop e verifique se você não possui mais conflitos:
Usando uma tabela com uma linha para eliminar deadlocks
Em vez de chamar sp_getapplock, podemos modificar a seguinte tabela:
Depois de criar e preencher esta tabela, podemos substituir a seguinte linha
com este, em ambos os procedimentos:
Você pode executar novamente o teste de estresse e verificar por si mesmo que não temos impasses.
Conclusão
Como vimos, o sp_getapplock pode ser usado para serializar o acesso a outros recursos. Como tal, ele pode ser usado para eliminar conflitos.
Obviamente, isso pode reduzir significativamente as modificações. Para resolver isso, precisamos escolher a granularidade certa para o bloqueio exclusivo e, sempre que possível, trabalhar com conjuntos em vez de linhas individuais.
Antes de usar essa abordagem, você precisa fazer o teste de estresse por conta própria. Primeiro, você precisa ter pelo menos uma dúzia de impasses com sua abordagem original. Segundo, você não deve obter conflitos quando executar novamente o mesmo script de reprodução usando o procedimento armazenado modificado.
Em geral, não acho que exista uma boa maneira de determinar se seu T-SQL está protegido contra conflitos apenas olhando para ele ou para o plano de execução. Na IMO, a única maneira de determinar se seu código está propenso a conflitos é 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.
fonte
UPDATE dbo.DeadlockTestMutex SET Toggle = 1 - Toggle WHERE ID = 1;
evita conflitos?O uso da
XLOCK
dica no seuSELECT
abordagem ou no seguinteUPDATE
deve ser imune a esse tipo de impasse:Retornará com algumas outras variantes (se não for batida!).
fonte
XLOCK
impeça a atualização de um contador existente de várias conexões, você não precisaria de umTABLOCKX
para impedir que várias conexões adicionassem o mesmo novo contador?Mike Defehr me mostrou uma maneira elegante de fazer isso de uma maneira muito leve:
(Para completar, eis a tabela associada ao processo 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 de conflito):
Claramente, a nova versão vence!
Para 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!
fonte
SERIALIZABLE
não existe para impedir fantasmas. Existe para fornecer semântica de isolamento serializável , ou seja, o mesmo efeito persistente no banco de dados como se as transações envolvidas tivessem sido executadas serialmente em alguma ordem não especificada.Não para roubar o trovão de Mark Storey-Smith, mas ele está em algo com o seu post acima (que recebeu incidentalmente o maior número de votos). O conselho que dei a Max estava centrado no construto "UPDATE set @variable = column = column + value", que eu acho muito legal, mas acho que pode não ser documentado (ele precisa ser suportado, embora seja específico para o TCP) benchmarks).
Aqui está uma variação da resposta de Mark - porque você está retornando o novo valor de ID como um conjunto de registros, pode acabar com a variável escalar completamente, nenhuma transação explícita também deve ser necessária e eu concordaria que mexer nos níveis de isolamento é desnecessário também. O resultado é muito limpo e bem liso ...
fonte
Corrigi um impasse semelhante em um sistema no ano passado, alterando isso:
Para isso:
Em geral, selecionar um
COUNT
apenas para determinar a presença ou ausência é um grande desperdício. Nesse caso, como é 0 ou 1, não é muito trabalhoso, mas (a) esse hábito pode desaparecer em outros casos em que será muito mais caro (nesses casos, use emIF NOT EXISTS
vez deIF COUNT() = 0
) e (b) a verificação adicional é completamente desnecessária. oUPDATE
executa essencialmente a mesma verificação.Além disso, isso me parece um cheiro sério de código:
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?fonte
IDENTITY
. Esta tabela suporta algum código legado escrito no MS Access que seria bastante envolvido na atualização. ASET @NewID=
linha simplesmente incrementa o valor armazenado na tabela para o ID fornecido (mas você já sabe disso). Você pode expandir como eu poderia usarROW_NUMBER()
?LastID
realmente significa no seu modelo. Qual é seu propósito? O nome não é exatamente auto-explicativo. Como o Access o usa?GetNextID('WhatevertheIDFieldIsCalled')
para obter o próximo ID a ser usado e o insere na nova linha, juntamente com os dados necessários.