MERGE prevenção de impasse

9

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 é

plano de execução de busca de índice

com o seguinte padrão de travamento

padrão de bloqueio de busca de índice

ou seja, IXbloqueie o objeto seguido por bloqueios mais granulares.

Às vezes, no entanto, o plano de execução da consulta é diferente

plano de execução da varredura de tabela

(essa forma de plano pode ser forçada adicionando uma INDEX(0)dica) e seu padrão de bloqueio é

padrão de bloqueio de verificação de tabela

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

gráfico de deadlock

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

mesclar holdlock tablock padrão de bloqueio

e com o TABLOCKXpadrão de bloqueio é

mesclar holdlock 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 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á

índice busca plano de execução com nvarchar

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?

i-one
fonte
Pedido relacionado em feedback.azure.com
i-one

Respostas:

9

A colocação IXseguida pelo Xobjeto é elegível? É bug ou não?

Parece um pouco estranho, mas é válido. No momento em que IXé tomada, a intenção pode muito bem ser Xtrancar 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 para ISe Sbloqueios 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 Xbloqueio no nível do objeto. Nesse sentido, o mecanismo poderá detectar precocemente que um Xbloqueio será inevitavelmente necessário se o método de acesso for uma varredura de heap e, portanto, evite tomá-lo IX.

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 IXpode 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.

Será que isso FORCESEEKserá ignorado e o padrão de bloqueio incorreto será usado?

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).

... declaração de variável de varchar(200)para nvarchar(200)...

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]....

Meu entendimento [...] é que o bloqueio é situacional em grande parte, e certa forma do plano de execução não implica certo padrão de bloqueio.

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:

CREATE UNIQUE CLUSTERED INDEX IX_Cache ON [Cache] ([ItemKey]);

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.

MERGEtambém é um pouco incomum de se ver em um ambiente de alta simultaneidade. Um tanto contra-intuitivamente, geralmente é mais eficiente executar declarações INSERTe UPDATEdeclarações separadas , por exemplo:

DECLARE
    @itemKey varchar(200) = 'Item_0F3C43A6A6A14255B2EA977EA730EDF2',
    @fileName nvarchar(255) = 'File_0F3C43A6A6A14255B2EA977EA730EDF2.dat';

BEGIN TRANSACTION;

    DECLARE @expires datetime2(2) = DATEADD(MINUTE, 10, SYSDATETIME());

    UPDATE TOP (1) dbo.Cache WITH (SERIALIZABLE, UPDLOCK)
    SET [FileName] = @fileName,
        Expires = @expires
    OUTPUT Deleted.[FileName]
    WHERE
        ItemKey = @itemKey;

    IF @@ROWCOUNT = 0
        INSERT dbo.Cache
            (ItemKey, [FileName], Expires)
        VALUES
            (@itemKey, @fileName, @expires);

COMMIT TRANSACTION;

Observe como a pesquisa do RID não é mais necessária:

Plano de execução

Se você pode garantir a existência de um índice exclusivo ItemKey(como na pergunta), o redundante TOP (1)no UPDATEpode ser removido, fornecendo o plano mais simples:

Atualização simplificada

Ambos INSERTe UPDATEplanos de se qualificar para um plano trivial em ambos os casos. MERGEsempre 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 .

Paul White 9
fonte