Rastreando, depurando e corrigindo contenções de bloqueio de linha

12

Até tarde, tenho enfrentado muitas contenções de bloqueio de linha. A tabela em disputa parece ser uma tabela específica.

Geralmente é o que acontece -

  • O desenvolvedor 1 inicia uma transação na tela front-end do Oracle Forms
  • O desenvolvedor 2 inicia outra transação, a partir de uma sessão diferente, usando a mesma tela

~ 5 minutos depois, o front end parece não responder. A verificação de sessões mostra a contenção de bloqueio de linha. A "solução" que todo mundo usa é matar sessões: /

Como desenvolvedor de banco de dados

  • O que pode ser feito para eliminar as contenções de bloqueio de linha?
  • Seria possível descobrir qual linha de um procedimento armazenado está causando essas contenções de bloqueio de linha
  • Qual seria a diretriz geral para reduzir / evitar / eliminar esses problemas que codificam?

Se essa pergunta parecer muito aberta / insuficiente, fique à vontade para editar / me avisar - farei o possível para adicionar algumas informações adicionais.


A tabela em questão está sujeita a muitas inserções e atualizações, eu diria que é uma das tabelas mais movimentadas. O SP é bastante complexo - para simplificar - busca dados de várias tabelas, preenche-os em tabelas de trabalho, muitas operações aritméticas ocorrem na tabela de trabalho e o resultado da tabela de trabalho é inserido / atualizado na tabela em questão.


A versão do banco de dados é o Oracle Database 10g Enterprise Edition, versão 10.2.0.1.0 - 64 bits. O fluxo da lógica é executado na mesma ordem nas duas sessões, a transação não é mantida aberta por muito tempo (ou pelo menos acho que sim) e os bloqueios ocorrem durante a execução ativa das transações.


Atualização: a contagem de linhas da tabela é maior do que eu esperava, com cerca de 3,1 milhões de linhas. Além disso, depois de rastrear uma sessão, descobri que algumas instruções de atualização nesta tabela não estão utilizando o índice. Por que é assim - não tenho certeza. A coluna referenciada na cláusula where é indexada. Atualmente, estou recriando o índice.

Sathyajith Bhat
fonte
1
@Athya - você pode elaborar a complexidade do procedimento armazenado? a tabela suspeita está sob atualização ou inserção rigorosa?
CoderHawk
As chaves estrangeiras desempenham um papel aqui? (Às vezes isso precisa de um índice) Qual versão do banco de dados existe? O fluxo da lógica é executado na mesma ordem nas duas sessões? A transação é mantida 'aberta' por um longo tempo? O bloqueio ocorre durante o tempo de reflexão dos usuários ou durante a execução ativa da transação?
Ik_zelf
@Sandy Eu atualizei a questão
Sathyajith Bhat
@ik_zelf Atualizei a pergunta #
Sathyajith Bhat
1
Não está claro para mim por que isso é um problema - a Oracle está fazendo exatamente o que deveria fazer, que é serializar o acesso a uma única linha. Se alguém tiver essa linha, você poderá ler a versão anterior, mas, para escrever, terá que esperar que ela libere o bloqueio. A única "solução" para isso é: a) não brincar e / COMMITou ROLLBACKem um tempo razoável ou b) organizar de modo que as mesmas pessoas nem sempre desejem a mesma linha ao mesmo tempo.
Gaius

Respostas:

10

Seria possível descobrir qual linha de um procedimento armazenado está causando essas contenções de bloqueio de linha?

Não exatamente, mas você pode obter a instrução SQL que está causando o bloqueio e, por sua vez, identificar as linhas relacionadas no procedimento.

SELECT sid, sql_text
FROM v$session s
LEFT JOIN v$sql q ON q.sql_id=s.sql_id
WHERE state = 'WAITING' AND wait_class != 'Idle'
AND event = 'enq: TX - row lock contention';

Qual seria a diretriz geral para reduzir / evitar / eliminar esses problemas com a codificação?

A seção Oracle Concepts Guide sobre bloqueios diz: "Uma linha é bloqueada somente quando modificada por um gravador". Outra sessão que atualiza a mesma linha aguardará a primeira sessão COMMITou ROLLBACKantes de continuar. Para eliminar o problema, você pode serializar os usuários, mas aqui estão algumas coisas que podem reduzir o problema, talvez ao nível de não ser um problema.

  • COMMITmais frequentemente. Todas as COMMITliberações são bloqueadas, portanto, se você pode fazer as atualizações em lotes, a probabilidade de outra sessão precisar da mesma linha é reduzida.
  • Verifique se você não está atualizando nenhuma linha sem alterar seus valores. Por exemplo, UPDATE t1 SET f1=DECODE(f2,’a’,f1+1,f1);deve ser reescrito como o mais seletivo (leia menos bloqueios) UPDATE t1 SET f1=f1+1 WHERE f2=’a’;. Obviamente, se a alteração da instrução ainda bloquear a maioria das linhas na tabela, a alteração terá apenas um benefício de legibilidade.
  • Verifique se você está usando sequências em vez de bloquear uma tabela para adicionar uma ao valor atual mais alto.
  • Verifique se você não está usando uma função que está fazendo com que um índice não seja usado. Se a função for necessária, considere torná-la um índice baseado em função.
  • Pense em conjuntos. Considere se um loop executando um bloco de PL / SQL fazendo atualizações pode ser reescrito como uma única instrução de atualização. Caso contrário, talvez o processamento em massa possa ser usado com BULK COLLECT ... FORALL.
  • Reduza o trabalho realizado entre o primeiro UPDATEe o COMMIT. Por exemplo, se o código enviar um email após cada atualização, considere enfileirar os emails e enviá-los após confirmar as atualizações.
  • Projete o aplicativo para lidar com a espera executando um SELECT ... FOR UPDATE NOWAITou WAIT 2. Você pode detectar a incapacidade de bloquear a linha e informar ao usuário que outra sessão está modificando os mesmos dados.
Leigh Riffel
fonte
7

Fornecerei uma resposta do ponto de vista do desenvolvedor.

Na minha opinião, quando você encontra uma disputa de linha como a que você descreve, é porque você tem um bug no seu aplicativo. Na maioria dos casos, esse tipo de contenção é um sinal de vulnerabilidade de atualização perdida. Este tópico no AskTom explica o conceito de uma atualização perdida:

Uma atualização perdida acontece quando:

sessão 1: leia o registro de funcionários de Tom

sessão 2: leia o registro de funcionários de Tom

sessão 1: atualizar o registro de funcionários de Tom

sessão 2: atualizar o registro de funcionários de Tom

A Sessão 2 ESCREVERÁ AS GRAVAÇÕES da sessão 1 sem nunca vê-las - resultando em uma atualização perdida.

Você experimentou um efeito colateral desagradável de atualização perdida: a sessão 2 pode ser bloqueada porque a sessão 1 ainda não foi confirmada. O principal problema, porém, é que a sessão 2 atualiza cegamente o registro. Suponha que ambas as sessões emitam a declaração:

UPDATE table SET col1=:col1, ..., coln=:coln WHERE id = :pk

Após as duas instruções, as modificações da sessão1 foram substituídas, sem que a sessão2 tenha sido notificada de que a linha foi modificada pela sessão 1.


A atualização perdida (e o efeito colateral da disputa) nunca deve acontecer, pois é 100% evitável. Você deve usar o bloqueio para evitá-los com dois métodos principais: bloqueio otimista e pessimista .

1) Bloqueio pessimista

Você deseja atualizar uma linha. Nesse modo, você impedirá que outras pessoas modifiquem essa linha solicitando um bloqueio nessa linha (SELECT ... FOR UPDATE NOWAIT instrução). Se a linha já estiver sendo modificada, você receberá uma mensagem de erro, que pode ser traduzida normalmente para o usuário final (esta linha está sendo modificada por outro usuário). Se a linha estiver disponível, faça suas modificações (UPDATE) e confirme sempre que sua transação for concluída.

2) Bloqueio otimista

Você deseja atualizar uma linha. No entanto, você não deseja manter um bloqueio nessa linha, talvez porque use várias transações para atualizar a linha (aplicativo sem estado na Web) ou talvez não queira que nenhum usuário mantenha um bloqueio por muito tempo ( o que pode resultar no bloqueio de outras pessoas). Nesse caso, você não solicitará um bloqueio imediatamente. Você usará um marcador para garantir que a linha não seja alterada quando sua atualização for emitida. Você pode armazenar em cache o valor de todas as colunas ou usar uma coluna de carimbo de data / hora atualizada automaticamente ou uma coluna baseada em sequência. Seja qual for a sua escolha, quando você estiver prestes a executar sua atualização, verifique se o marcador nessa linha não foi alterado emitindo uma consulta como:

SELECT <...>
  FROM table
 WHERE id = :id
   AND marker = :marker
   FOR UPDATE NOWAIT

Se a consulta retornar uma linha, faça sua atualização. Caso contrário, isso significa que alguém modificou a linha desde a última vez que você a consultou. Você terá que reiniciar o processo desde o início.

Nota: Se você tiver uma confiança completa em todos os aplicativos que acessam seu banco de dados, poderá confiar em uma atualização direta para o bloqueio otimista. Você pode emitir diretamente:

UPDATE table
   SET <...>, 
       marker = marker + 1
 WHERE id = :id;

Se a instrução não atualizar nenhuma linha, você saberá que alguém alterou essa linha e precisará começar tudo de novo.

Se todos os aplicativos concordarem com esse esquema, você nunca seria bloqueado por outra pessoa e evitaria a atualização cega. No entanto, se você não bloquear a linha antecipadamente, ainda estará suscetível ao bloqueio indefinido se outro aplicativo, trabalho em lote ou atualização direta não implementar o bloqueio otimista. É por isso que aconselho sempre bloquear a linha, qualquer que seja sua opção de esquema de bloqueio (a ocorrência de desempenho pode ser insignificante, pois você recupera todos os valores, incluindo o rowid ao bloquear a linha).

TL; DR

  • A atualização de uma linha sem bloquear previamente expõe o aplicativo a um possível "congelamento". Isso pode ser evitado se todo o DML no banco de dados implementar bloqueio otimista ou pessimista.
  • Verifique se a instrução SELECT retorna valores consistentes com qualquer SELECT anterior (para evitar qualquer problema de atualização perdida)
Vincent Malgrat
fonte
5

Essa resposta provavelmente se qualificaria para uma entrada no The Daily WTF.

Certo, depois de rastrear as sessões e pesquisar USER_SOURCE- eu rastreei a causa raiz

  • A causa, sem surpresa, foi falha na lógica
  • Recentemente, uma declaração de atualização foi adicionada ao SP. A instrução update basicamente atualizaria a tabela inteira. Aparentemente, o desenvolvedor em questão esqueceu de adicionar as cláusulas where where para atualizar as instruções necessárias.
  • A tabela que está sendo atualizada foi como mencionado acima, uma das tabelas mais negociadas e possuía um grande número de registros. A atualização levaria um tempo longo e angustiante.
  • O resultado foi que outras sessões não foram capazes de bloquear a mesa e permaneceriam em contenções de bloqueio de linha.
Sathyajith Bhat
fonte