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 HOLDLOCKestá 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, IXbloqueie 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 é
Xtrava de aviso colocada no objeto depois de IXjá estar colocada.
Como dois IXsão compatíveis, mas dois Xnão, a coisa que acontece em simultâneo é
impasse !
E aqui surge a primeira parte da pergunta . A colocação do Xbloqueio no objeto é IXelegí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 Xbloqueio no objeto depois IXparece 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 TABLOCKpadrão de travamento no lugar
e com o TABLOCKXpadrã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 PAGLOCKe ROWLOCKtornar os bloqueios mais granulares e reduzir a contenção. Ambos não têm efeito (o Xobjeto 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 FORCESEEKdica
MERGE INTO [Cache] WITH (HOLDLOCK, FORCESEEK(IX_Cache(ItemKey))) T
e funcionou.
E aqui surge a segunda parte da questão . Será que isso FORCESEEKserá ignorado e o padrão de bloqueio incorreto será usado? (Como eu mencionei, PAGLOCKe ROWLOCKforam aparentemente ignorados).
A adição UPDLOCKnão tem efeito ( Xno 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 @itemKeydeclaraçã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 Xbloqueio 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 Xbloqueio no objeto depois de IXaberto. E se for elegível, há algo que se possa fazer para impedir o bloqueio de objetos?










