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?