Recentemente, um de nossos aplicativos ASP.NET exibiu um erro de deadlock de banco de dados e fui solicitado a verificar e corrigir o erro. Consegui descobrir que a causa do impasse era um procedimento armazenado que estava atualizando rigorosamente uma tabela dentro de um cursor.
Esta é a primeira vez que vejo esse erro e não sabia como rastreá-lo e corrigi-lo de maneira eficaz. Eu tentei todas as formas possíveis que conheço e finalmente descobri que a tabela que está sendo atualizada não possui uma chave primária! felizmente era uma coluna de identidade.
Mais tarde, encontrei o desenvolvedor que fez o script do banco de dados para implantação confuso. Eu adicionei uma chave primária e o problema foi resolvido.
Fiquei feliz e voltei ao meu projeto, e fiz algumas pesquisas para descobrir o motivo daquele impasse...
Aparentemente, foi uma condição de espera circular que causou o impasse. As atualizações aparentemente demoram mais sem uma chave primária do que com a chave primária.
Eu sei que não é uma conclusão bem definida, por isso estou postando aqui...
- A chave primária ausente é o problema?
- Existem outras condições que causam impasse além de (exclusão mútua, espera e espera, sem preempção e espera circular)?
- Como evitar e rastrear impasses?
rastrear impasses é o mais fácil dos dois:
A prevenção é mais difícil, essencialmente você deve estar atento ao seguinte:
O bloco de código 1 bloqueia o recurso A e, em seguida, o recurso B, nessa ordem.
O bloco de código 2 bloqueia o recurso B e, em seguida, o recurso A, nessa ordem.
Esta é a condição clássica em que um deadlock pode ocorrer, se o bloqueio de ambos os recursos não for atômico, o Bloco de Código 1 pode bloquear A e ser antecipado, então o Bloco de Código 2 bloqueia B antes que A recupere o tempo de processamento. Agora você tem impasse.
Para evitar essa condição, você pode fazer algo como o seguinte
Bloco de código A (pseudo código)
Bloco de código B (pseudo código)
não esquecendo de desbloquear A e B quando terminar com eles
isso evitaria o bloqueio entre o bloco de código A e o bloco de código B
Do ponto de vista do banco de dados, não tenho certeza de como evitar essa situação, pois os bloqueios são tratados pelo próprio banco de dados, ou seja, bloqueios de linha/tabela ao atualizar dados. Onde eu vi a maioria dos problemas ocorrerem é onde você viu o seu, dentro de um cursor. Os cursores são notoriamente ineficientes, evite-os se possível.
meus artigos favoritos para ler e aprender sobre deadlocks são: Simple Talk - Rastreie deadlocks e SQL Server Central - Using Profiler to solve deadlocks . Eles lhe darão amostras e conselhos sobre como lidar com uma situação ruim.
Resumindo, para resolver um problema atual, eu faria as transações envolvidas mais curtas, tiraria a parte desnecessária delas, cuidaria da ordem de uso dos objetos, veria qual nível de isolamento é realmente necessário, não leria desnecessário dados...
Mas é melhor ler os artigos, eles serão muito mais legais em conselhos.
Às vezes, um impasse pode ser resolvido adicionando indexação, pois permite que o banco de dados bloqueie registros individuais em vez de toda a tabela, reduzindo a contenção e a possibilidade de as coisas ficarem congestionadas.
Por exemplo, no InnoDB :
Outra solução comum é desativar a consistência transacional quando não for necessária ou alterar seu nível de isolamento , por exemplo, um trabalho de longa duração para calcular estatísticas ... uma resposta próxima geralmente é suficiente, você não precisa de números precisos, como eles estão mudando debaixo de você. E se levar 30 minutos para ser concluído, você não quer que ele interrompa todas as outras transações nessas tabelas.
...
Quanto a rastreá-los, depende do software de banco de dados que você está usando.
Só para desenvolver na coisa do cursor. é realmente muito ruim. Ele bloqueia a tabela inteira e processa as linhas uma a uma.
É melhor percorrer as linhas na forma de um cursor usando um loop while
No loop while, uma seleção será realizada para cada linha do loop e o bloqueio ocorrerá em apenas uma linha por vez. O restante dos dados na tabela é livre para consulta, reduzindo assim as chances de ocorrer um deadlock.
Além disso, é mais rápido. Faz você se perguntar por que existem cursores de qualquer maneira.
Veja um exemplo desse tipo de estrutura:
Se o seu campo de ID for esparso, você pode querer puxar uma lista separada de IDs e iterar por ela:
A falta de uma chave primária não é o problema. Pelo menos por si só. Primeiro, você não precisa de um primário para ter índices. Segundo, mesmo se você estiver fazendo varreduras de tabela (o que deve acontecer se sua consulta em particular não estiver usando um índice, um bloqueio de tabela não causará um deadlock por si só. Um processo de gravação esperaria por uma leitura e um processo de leitura esperar por uma gravação e, é claro, as leituras não precisariam esperar uma pela outra.
Adicionando às outras respostas, o nível de isolamento da transação é importante, porque a leitura repetível e a serialização são o que faz com que os bloqueios de 'leitura' sejam mantidos até o final da transação. Bloquear um recurso não causa um deadlock. Mantê-lo trancado sim. As operações de gravação sempre mantêm seus recursos bloqueados até o final da transação.
Minha estratégia de prevenção de bloqueio favorita é usar os recursos de 'instantâneo'. O recurso Read Committed Snapshot significa que as leituras não usam bloqueios! E se você precisar de mais controle do que 'Leitura confirmada', há o recurso 'Nível de isolamento de captura instantânea'. Este permite que uma transação serializada (usando os termos do MS aqui) ocorra sem bloquear os outros jogadores.
Por fim, uma classe de deadlocks pode ser evitada usando um bloqueio de atualização. Se você ler e segurar a leitura (HOLD, ou usando Repeatable Read), e outro processo fizer o mesmo, então ambos tentarem atualizar os mesmos registros, você terá um deadlock. Mas se ambos solicitarem um bloqueio de atualização, o segundo processo aguardará o primeiro, enquanto permite que outros processos leiam os dados usando bloqueios compartilhados até que os dados sejam realmente gravados. Obviamente, isso não funcionará se um dos processos ainda solicitar um bloqueio HOLD compartilhado.
Embora os cursores sejam lentos no SQL Server, você pode evitar o bloqueio em um cursor puxando os dados de origem do cursor para uma tabela Temp e executando o cursor nela. Isso evita que o cursor bloqueie a tabela de dados real e os únicos bloqueios que você obtém são para as atualizações ou inserções realizadas dentro do cursor, que são mantidas apenas pela duração da inserção/atualização e não pela duração do cursor.