Declaração de mesclagem em conflito

22

Eu tenho o seguinte procedimento (SQL Server 2008 R2):

create procedure usp_SaveCompanyUserData
    @companyId bigint,
    @userId bigint,
    @dataTable tt_CoUserdata readonly
as
begin

    set nocount, xact_abort on;

    merge CompanyUser with (holdlock) as r
    using (
        select 
            @companyId as CompanyId, 
            @userId as UserId, 
            MyKey, 
            MyValue
        from @dataTable) as newData
    on r.CompanyId = newData.CompanyId
        and r.UserId = newData.UserId
        and r.MyKey = newData.MyKey
    when not matched then
        insert (CompanyId, UserId, MyKey, MyValue) values
        (@companyId, @userId, newData.MyKey, newData.MyValue);

end;

CompanyId, UserId, MyKey formam a chave composta para a tabela de destino. CompanyId é uma chave estrangeira para uma tabela pai. Além disso, há um índice não agrupado em CompanyId asc, UserId asc.

É chamado de muitos segmentos diferentes, e eu estou constantemente obtendo impasses entre diferentes processos chamando essa mesma declaração. Meu entendimento era que o "with (holdlock)" era necessário para impedir a inserção / atualização de erros de condição de corrida.

Suponho que dois segmentos diferentes estejam bloqueando linhas (ou páginas) em ordens diferentes quando validam as restrições e, portanto, estão em conflito.

Será esta uma suposição correta?

Qual é a melhor maneira de resolver essa situação (ou seja, sem impasses, impacto mínimo no desempenho multithread)?

Imagem do plano de consulta (Se você visualizar a imagem em uma nova guia, ela ficará legível. Desculpe pelo tamanho pequeno.)

  • Existem no máximo 28 linhas no @datatable.
  • Eu rastreei o código e não consigo ver em nenhum lugar que iniciamos uma transação aqui.
  • A chave estrangeira é configurada para cascatear apenas na exclusão e não houve exclusões na tabela pai.
Sako73
fonte

Respostas:

12

OK, depois de analisar tudo várias vezes, acho que sua suposição básica estava correta. O que provavelmente está acontecendo aqui é o seguinte:

  1. A parte MATCH do MERGE verifica o índice em busca de correspondências, bloqueando a leitura dessas linhas / páginas à medida que avança.

  2. Quando tiver uma linha sem correspondência, ele tentará inserir a nova linha de índice primeiro para solicitar um bloqueio de gravação de linha / página ...

Mas se outro usuário também chegou à etapa 1 na mesma linha / página, o primeiro usuário será bloqueado na Atualização e ...

Se o segundo usuário também precisar inserir na mesma página, ele estará em um impasse.

AFAIK, existe apenas uma maneira (simples) de ter 100% de certeza de que você não pode obter um impasse com este procedimento e que seria adicionar uma dica TABLOCKX ao MERGE, mas isso provavelmente teria um impacto muito ruim no desempenho.

É possível que a adição de uma dica TABLOCK seja suficiente para resolver o problema sem afetar muito o desempenho.

Por fim, você também pode tentar adicionar PAGLOCK, XLOCK ou PAGLOCK e XLOCK. Novamente, isso pode funcionar e o desempenho pode não ser muito ruim. Você terá que tentar ver.

RBarryYoung
fonte
Você acha que o nível de isolamento de captura instantânea (controle de versão de linha) pode ser útil aqui?
Mikael Eriksson
Talvez. Ou pode transformar as exceções de deadlock em exceções de simultaneidade.
RBarryYoung
2
Especificar a dica TABLOCK em uma tabela que é o destino de uma instrução INSERT tem o mesmo efeito que especificar a dica TABLOCKX. (Fonte: msdn.microsoft.com/en-us/library/bb510625.aspx )
tuespetre
31

Não haveria problema se a variável da tabela tivesse apenas um valor. Com várias linhas, há uma nova possibilidade de conflito. Suponha que dois processos simultâneos (A e B) sejam executados com variáveis ​​de tabela contendo (1, 2) e (2, 1) para a mesma empresa.

O processo A lê o destino, não encontra linha e insere o valor '1'. Ele contém um bloqueio de linha exclusivo no valor '1'. O processo B lê o destino, não encontra linha e insere o valor '2'. Ele contém um bloqueio de linha exclusivo no valor '2'.

Agora, o processo A precisa processar a linha 2 e o processo B precisa processar a linha 1. Nenhum processo pode progredir porque requer um bloqueio que é incompatível com o bloqueio exclusivo mantido pelo outro processo.

Para evitar conflitos com várias linhas, as linhas precisam ser processadas (e as tabelas acessadas) na mesma ordem todas as vezes . A variável de tabela no plano de execução mostrada na pergunta é uma pilha, portanto as linhas não têm ordem intrínseca (é bem provável que sejam lidas em ordem de inserção, embora isso não seja garantido):

Plano existente

A falta de ordem consistente de processamento de linha leva diretamente à oportunidade de conflito. Uma segunda consideração é que a falta de uma garantia de exclusividade importante significa que um spool de tabela é necessário para fornecer a proteção correta de Halloween. O spool é um spool ansioso, o que significa que todas as linhas são gravadas em uma mesa de trabalho tempdb antes de serem lidas novamente e reproduzidas para o operador Insert.

Redefinindo a TYPEvariável da tabela para incluir um cluster PRIMARY KEY:

DROP TYPE dbo.CoUserData;

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL PRIMARY KEY CLUSTERED,
    MyValue integer NOT NULL
);

O plano de execução agora mostra uma varredura do índice em cluster e a garantia de exclusividade significa que o otimizador pode remover com segurança o spool de tabela:

Com chave primária

Nos testes com 5000 iterações da MERGEinstrução em 128 threads, nenhum conflito ocorreu com a variável de tabela em cluster. Devo enfatizar que isso é apenas com base na observação; a variável de tabela em cluster também pode ( tecnicamente ) produzir suas linhas em uma variedade de pedidos, mas as chances de um pedido consistente são bastante aumentadas. O comportamento observado precisaria ser testado novamente para cada nova atualização cumulativa, service pack ou nova versão do SQL Server, é claro.

Caso a definição da variável da tabela não possa ser alterada, existe outra alternativa:

MERGE dbo.CompanyUser AS R
USING 
    (SELECT DISTINCT MyKey, MyValue FROM @DataTable) AS NewData ON
    R.CompanyId = @CompanyID
    AND R.UserID = @UserID
    AND R.MyKey = NewData.MyKey
WHEN NOT MATCHED THEN 
    INSERT 
        (CompanyID, UserID, MyKey, MyValue) 
    VALUES
        (@CompanyID, @UserID, NewData.MyKey, NewData.MyValue)
OPTION (ORDER GROUP);

Isso também alcança a eliminação do spool (e a consistência da ordem das linhas) ao custo da introdução de uma classificação explícita:

Classificar plano

Esse plano também não produziu conflitos usando o mesmo teste. Roteiro de reprodução abaixo:

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL /* PRIMARY KEY */,
    MyValue integer NOT NULL
);
GO
CREATE TABLE dbo.Company
(
    CompanyID   integer NOT NULL

    CONSTRAINT PK_Company
        PRIMARY KEY (CompanyID)
);
GO
CREATE TABLE dbo.CompanyUser
(
    CompanyID   integer NOT NULL,
    UserID      integer NOT NULL,
    MyKey       integer NOT NULL,
    MyValue     integer NOT NULL

    CONSTRAINT PK_CompanyUser
        PRIMARY KEY CLUSTERED
            (CompanyID, UserID, MyKey),

    FOREIGN KEY (CompanyID)
        REFERENCES dbo.Company (CompanyID),
);
GO
CREATE NONCLUSTERED INDEX nc1
ON dbo.CompanyUser (CompanyID, UserID);
GO
INSERT dbo.Company (CompanyID) VALUES (1);
GO
DECLARE 
    @DataTable AS dbo.CoUserData,
    @CompanyID integer = 1,
    @UserID integer = 1;

INSERT @DataTable
SELECT TOP (10)
    V.MyKey,
    V.MyValue
FROM
(
    VALUES
        (1, 1),
        (2, 2),
        (3, 3),
        (4, 4),
        (5, 5),
        (6, 6),
        (7, 7),
        (8, 8),
        (9, 9)
) AS V (MyKey, MyValue)
ORDER BY NEWID();

BEGIN TRANSACTION;

    -- Test MERGE statement here

ROLLBACK TRANSACTION;
Paul White diz que a GoFundMonica
fonte
8

Eu acho que o SQL_Kiwi forneceu uma análise muito boa. Se você precisar resolver o problema no banco de dados, siga sua sugestão. É claro que você precisa testar novamente que ele ainda funciona para você sempre que atualizar, aplicar um service pack ou adicionar / alterar um índice ou uma exibição indexada.

Existem outras três alternativas:

  1. Você pode serializar suas inserções para que não colidam: você pode chamar sp_getapplock no início de sua transação e adquirir um bloqueio exclusivo antes de executar seu MERGE. Claro que você ainda precisa se esforçar para testá-lo.

  2. Você pode ter um thread para lidar com todas as inserções, para que o servidor de aplicativos lide com a simultaneidade.

  3. Você pode tentar novamente automaticamente após conflitos - essa pode ser a abordagem mais lenta se a simultaneidade for alta.

De qualquer forma, somente você pode determinar o impacto da sua solução no desempenho.

Normalmente, não temos impasses em nosso sistema, embora tenhamos muito potencial para tê-los. Em 2011, cometemos um erro em uma implantação e tivemos meia dúzia de deadlocks em poucas horas, todos seguindo o mesmo cenário. Eu consertei isso em breve e esses foram todos os impasses do ano.

Estamos usando principalmente a abordagem 1 em nosso sistema. Funciona muito bem para nós.

AK
fonte
-1

Outra abordagem possível - achei que o Merge às vezes apresenta problemas de bloqueio e desempenho - pode valer a pena jogar com a opção de consulta Option (MaxDop x)

No passado sombrio e distante, o SQL Server tinha uma opção Inserir bloqueio no nível da linha - mas isso parece ter morrido, no entanto, uma PK em cluster com uma identidade deve fazer com que as inserções sejam executadas de maneira limpa.

Ed Green
fonte