Em um de nossos bancos de dados, temos uma tabela que é acessada intensivamente simultaneamente por vários threads. Threads atualizam ou inserem linhas via MERGE
. Também existem threads que excluem linhas ocasionalmente; portanto, os dados da tabela são muito voláteis. Tópicos que fazem upserts sofrem de impasse às vezes. O problema é semelhante ao descrito nesta pergunta. A diferença, porém, é que, no nosso caso, cada thread atualiza ou insere exatamente uma linha .
A seguir, a configuração simplificada. A tabela é montada com dois índices não clusterizados exclusivos sobre
CREATE TABLE [Cache]
(
[UID] uniqueidentifier NOT NULL CONSTRAINT DF_Cache_UID DEFAULT (newid()),
[ItemKey] varchar(200) NOT NULL,
[FileName] nvarchar(255) NOT NULL,
[Expires] datetime2(2) NOT NULL,
CONSTRAINT [PK_Cache] PRIMARY KEY NONCLUSTERED ([UID])
)
GO
CREATE UNIQUE INDEX IX_Cache ON [Cache] ([ItemKey]);
GO
e a consulta típica é
DECLARE
@itemKey varchar(200) = 'Item_0F3C43A6A6A14255B2EA977EA730EDF2',
@fileName nvarchar(255) = 'File_0F3C43A6A6A14255B2EA977EA730EDF2.dat';
MERGE INTO [Cache] WITH (HOLDLOCK) T
USING (
VALUES (@itemKey, @fileName, dateadd(minute, 10, sysdatetime()))
) S(ItemKey, FileName, Expires)
ON T.ItemKey = S.ItemKey
WHEN MATCHED THEN
UPDATE
SET
T.FileName = S.FileName,
T.Expires = S.Expires
WHEN NOT MATCHED THEN
INSERT (ItemKey, FileName, Expires)
VALUES (S.ItemKey, S.FileName, S.Expires)
OUTPUT deleted.FileName;
ou seja, a correspondência acontece por chave de índice exclusiva. A dica HOLDLOCK
está aqui, devido à simultaneidade (conforme recomendado aqui ).
Eu fiz uma pequena investigação e o seguinte foi o que encontrei.
Na maioria dos casos, o plano de execução da consulta é
com o seguinte padrão de travamento
ou seja, IX
bloqueie o objeto seguido por bloqueios mais granulares.
Às vezes, no entanto, o plano de execução da consulta é diferente
(essa forma de plano pode ser forçada adicionando uma INDEX(0)
dica) e seu padrão de bloqueio é
X
trava de aviso colocada no objeto depois de IX
já estar colocada.
Como dois IX
são compatíveis, mas dois X
não, a coisa que acontece em simultâneo é
impasse !
E aqui surge a primeira parte da pergunta . A colocação do X
bloqueio no objeto é IX
elegível? Não é bug?
A documentação declara:
Os bloqueios de intenção são denominados bloqueios de intenção porque são adquiridos antes de um bloqueio no nível inferior e, portanto, sinalizam a intenção de colocar bloqueios em um nível inferior .
e também
IX significa a intenção de atualizar apenas algumas das linhas, em vez de todas elas
então, colocar X
bloqueio no objeto depois IX
parece muito suspeito para mim.
Primeiro, tentei evitar conflitos, tentando adicionar dicas de bloqueio de tabela
MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCK) T
e
MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCKX) T
com o TABLOCK
padrão de travamento no lugar
e com o TABLOCKX
padrão de bloqueio é
como dois SIX
(e dois X
) não são compatíveis, isso evita um impasse de maneira eficaz, mas, infelizmente, também impede a simultaneidade (o que não é desejado).
Minhas próximas tentativas foram adicionar PAGLOCK
e ROWLOCK
tornar os bloqueios mais granulares e reduzir a contenção. Ambos não têm efeito (o X
objeto ainda foi observado imediatamente após IX
).
Minha tentativa final foi forçar a "boa" forma do plano de execução com bom bloqueio granular, adicionando FORCESEEK
dica
MERGE INTO [Cache] WITH (HOLDLOCK, FORCESEEK(IX_Cache(ItemKey))) T
e funcionou.
E aqui surge a segunda parte da questão . Será que isso FORCESEEK
será ignorado e o padrão de bloqueio incorreto será usado? (Como eu mencionei, PAGLOCK
e ROWLOCK
foram aparentemente ignorados).
A adição UPDLOCK
não tem efeito ( X
no objeto ainda observável depois IX
).
Fazer o IX_Cache
índice agrupado, como previsto, funcionou. Isso levou ao planejamento com a busca de índice em cluster e o bloqueio granular. Além disso, tentei forçar a verificação de índice em cluster que também mostrava bloqueio granular.
Contudo. Observação adicional. Na configuração original, mesmo quando FORCESEEK(IX_Cache(ItemKey)))
em vigor, se uma @itemKey
declaração de variável for alterada de varchar (200) para nvarchar (200) , o plano de execução se tornará
veja que a busca é usada, MAS, neste caso, o padrão de bloqueio mostra novamente o X
bloqueio colocado no objeto depois IX
.
Portanto, parece que a busca forçada não garante necessariamente bloqueios granulares (e a ausência de conflitos). Não estou confiante de que o índice clusterizado garanta um bloqueio granular. Ou faz?
Meu entendimento (corrija-me se estiver errado) é que o bloqueio é situacional em grande parte, e certa forma do plano de execução não implica certo padrão de bloqueio.
A pergunta sobre a elegibilidade de colocar o X
bloqueio no objeto depois de IX
aberto. E se for elegível, há algo que se possa fazer para impedir o bloqueio de objetos?
Respostas:
Parece um pouco estranho, mas é válido. No momento em que
IX
é tomada, a intenção pode muito bem serX
trancar em um nível mais baixo. Não há nada a dizer que esses bloqueios devem ser realmente executados. Afinal, pode não haver nada para bloquear no nível mais baixo; o mecanismo não pode saber disso com antecedência. Além disso, pode haver otimizações para que os bloqueios de nível inferior possam ser ignorados (um exemplo paraIS
eS
bloqueios podem ser vistos aqui ).Mais especificamente para o cenário atual, é verdade que os bloqueios de intervalo de chaves serializáveis não estão disponíveis para um heap, portanto, a única alternativa é um
X
bloqueio no nível do objeto. Nesse sentido, o mecanismo poderá detectar precocemente que umX
bloqueio será inevitavelmente necessário se o método de acesso for uma varredura de heap e, portanto, evite tomá-loIX
.Por outro lado, o bloqueio é complexo e, às vezes, os bloqueios de intenção podem ser executados por razões internas, não necessariamente relacionadas à intenção de bloquear bloqueios de nível inferior. A tomada
IX
pode ser a maneira menos invasiva de fornecer a proteção necessária para alguns casos de borda obscuros. Para uma consideração semelhante, consulte Bloqueio compartilhado emitido em IsolationLevel.ReadUncommitted .Portanto, a situação atual é lamentável para o seu cenário de conflito, e pode ser evitável em princípio, mas isso não é necessariamente o mesmo que ser um 'bug'. Você pode relatar o problema pelo canal de suporte normal ou pelo Microsoft Connect, se precisar de uma resposta definitiva.
Não.
FORCESEEK
É menos uma dica e mais uma diretiva. Se o otimizador não conseguir encontrar um plano que respeite a 'dica', ele produzirá um erro.Forçar o índice é uma maneira de garantir que os bloqueios de intervalo de teclas possam ser executados. Juntamente com os bloqueios de atualização obtidos naturalmente ao processar um método de acesso para que as linhas sejam alteradas, isso fornece uma garantia suficiente para evitar problemas de simultaneidade no seu cenário.
Se o esquema da tabela não for alterado (por exemplo, adicionando um novo índice), a dica também será suficiente para evitar que essa consulta entre em conflito. Ainda existe a possibilidade de um conflito cíclico com outras consultas que possam acessar o heap antes do índice não clusterizado (como uma atualização da chave do índice não clusterizado).
Isso quebra a garantia de que uma única linha será afetada; portanto, um Epool Table Spool é introduzido para proteção de Halloween. Como alternativa adicional para isso, torne explícita a garantia
MERGE TOP (1) INTO [Cache]...
.Certamente há muito mais acontecendo que é visível em um plano de execução. Você pode forçar uma determinada forma de plano com, por exemplo, um guia de plano, mas o mecanismo ainda pode decidir fazer bloqueios diferentes em tempo de execução. As chances são razoavelmente baixas se você incorporar o
TOP (1)
elemento acima.Observações gerais
É um tanto incomum ver uma tabela de pilha sendo usada dessa maneira. Você deve considerar o mérito de convertê-lo em uma tabela em cluster, talvez usando o índice que Dan Guzman sugeriu em um comentário:
Isso pode ter importantes vantagens de reutilização de espaço, além de fornecer uma boa solução alternativa para o atual problema de conflito.
MERGE
também é um pouco incomum de se ver em um ambiente de alta simultaneidade. Um tanto contra-intuitivamente, geralmente é mais eficiente executar declaraçõesINSERT
eUPDATE
declarações separadas , por exemplo:Observe como a pesquisa do RID não é mais necessária:
Se você pode garantir a existência de um índice exclusivo
ItemKey
(como na pergunta), o redundanteTOP (1)
noUPDATE
pode ser removido, fornecendo o plano mais simples:Ambos
INSERT
eUPDATE
planos de se qualificar para um plano trivial em ambos os casos.MERGE
sempre exige otimização total baseada em custos.Consulte o problema de entrada Simultânea do SQL Server 2014 Concurrent relacionado para obter o padrão correto a ser usado e mais informações sobre
MERGE
.Os impasses nem sempre podem ser evitados. Eles podem ser reduzidos ao mínimo com codificação e design cuidadosos, mas o aplicativo deve sempre estar preparado para lidar com o impasse ímpar normalmente (por exemplo, verificar novamente as condições e tentar novamente).
Se você tiver controle total sobre os processos que acessam o objeto em questão, considere também usar bloqueios de aplicativos para serializar o acesso a elementos individuais, conforme descrito em Inserções e exclusões simultâneas do SQL Server .
fonte