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)?
(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.
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):
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
TYPE
variável da tabela para incluir um clusterPRIMARY KEY
: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:
Nos testes com 5000 iterações da
MERGE
instruçã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:
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:
Esse plano também não produziu conflitos usando o mesmo teste. Roteiro de reprodução abaixo:
fonte
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:
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.
Você pode ter um thread para lidar com todas as inserções, para que o servidor de aplicativos lide com a simultaneidade.
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.
fonte
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.
fonte