Como evitar o bloqueio do mysql 'encontrado ao tentar obter o bloqueio; tente reiniciar a transação '

286

Eu tenho uma tabela innoDB que registra usuários online. Ele é atualizado a cada atualização de página por um usuário para acompanhar quais páginas estão e sua última data de acesso ao site. Em seguida, tenho um cron que é executado a cada 15 minutos para excluir registros antigos.

Eu encontrei um 'Deadlock encontrado ao tentar trancar; tente reiniciar a transação 'por cerca de 5 minutos na noite passada e parece estar executando os INSERTs nesta tabela. Alguém pode sugerir como evitar esse erro?

=== EDIT ===

Aqui estão as consultas que estão sendo executadas:

Primeira visita ao site:

INSERT INTO onlineusers SET
ip = 123.456.789.123,
datetime = now(),
userid = 321,
page = '/thispage',
area = 'thisarea',
type = 3

Em cada atualização de página:

UPDATE onlineusers SET
ips = 123.456.789.123,
datetime = now(),
userid = 321,
page = '/thispage',
area = 'thisarea',
type = 3
WHERE id = 888

Cron a cada 15 minutos:

DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND

Em seguida, faz algumas considerações para registrar algumas estatísticas (ou seja, membros online, visitantes online).

David
fonte
Você pode fornecer mais detalhes sobre a estrutura da tabela? Existem índices agrupados ou não clusterizados?
Anders Abel
13
dev.mysql.com/doc/refman/5.1/en/innodb-deadlocks.html - A execução de "show status do innodb do mecanismo" forneceria diagnósticos úteis.
Martin
Não é uma boa prática fazer uma gravação de banco de dados síncrona quando os usuários pisam em uma página. a maneira correta de fazer isso é mantê-lo na memória, como memcache ou alguma fila rápida, e gravar no db com cron.
Nir

Respostas:

292

Um truque fácil que pode ajudar com a maioria dos impasses é classificar as operações em uma ordem específica.

Você obtém um impasse quando duas transações estão tentando bloquear dois bloqueios em ordens opostas, ou seja:

  • conexão 1: trava a chave (1), trava a chave (2);
  • conexão 2: trava a chave (2), trava a chave (1);

Se os dois rodarem ao mesmo tempo, a conexão 1 bloqueará a chave (1), a conexão 2 bloqueará a chave (2) e cada conexão aguardará que a outra libere a tecla -> deadlock.

Agora, se você alterou suas consultas para que as conexões travassem as chaves na mesma ordem, ou seja:

  • conexão 1: trava a chave (1), trava a chave (2);
  • conexão 2: trava a chave ( 1 ), trava a chave ( 2 );

será impossível obter um impasse.

Então é isso que eu sugiro:

  1. Verifique se não há outras consultas que bloqueiam o acesso a mais de uma chave por vez, exceto a instrução delete. se você fizer (e eu suspeito que sim), encomende o WHERE em (k1, k2, .. kn) em ordem crescente.

  2. Corrija sua instrução de exclusão para funcionar em ordem crescente:

mudança

DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND

Para

DELETE FROM onlineusers WHERE id IN (SELECT id FROM onlineusers
    WHERE datetime <= now() - INTERVAL 900 SECOND order by id) u;

Outra coisa a ter em mente é que a documentação do mysql sugere que, no caso de um conflito, o cliente deve tentar novamente automaticamente. você pode adicionar essa lógica ao código do seu cliente. (Digamos, 3 tentativas nesse erro específico antes de desistir).

Omry Yadan
fonte
2
Se eu tiver transação (confirmação automática = false), exceção de conflito lançada. É o suficiente apenas para repetir a mesma declaração .executeUpdate () ou a transação inteira agora está limitada e deve ser revertida + executar novamente tudo o que estava em execução nela?
Quem
5
se você tiver transações ativadas, é tudo ou nada. se você tiver uma exceção de qualquer tipo, é garantido que toda a transação não tenha efeito. nesse caso, você deseja reiniciar a coisa toda.
Omry Yadan 16/09
4
A exclusão com base em um seleto em uma enorme mesa é muito mais lento do que a simples eliminação
Thermech
3
Muito obrigado, cara. A dica 'instruções de classificação' corrigiu meus problemas de bloqueio morto.
25315
4
@OmryYadan Pelo que sei, no MySQL você não pode selecionar uma subconsulta da mesma tabela em que está fazendo o UPDATE. dev.mysql.com/doc/refman/5.7/en/update.html
artaxerxe
72

O conflito ocorre quando duas transações se esperam para adquirir um bloqueio. Exemplo:

  • Tx 1: bloquear A e, em seguida, B
  • Tx 2: bloqueio B e depois A

Existem inúmeras perguntas e respostas sobre conflitos. Cada vez que você insere / atualiza ou exclui uma linha, um bloqueio é adquirido. Para evitar deadlock, você deve garantir que as transações simultâneas não atualizem a linha em uma ordem que possa resultar em um deadlock. De um modo geral, tente adquirir o bloqueio sempre na mesma ordem, mesmo em transações diferentes (por exemplo, sempre a tabela A primeiro e depois a tabela B).

Outro motivo para o conflito no banco de dados pode estar faltando índices . Quando uma linha é inserida / atualizada / excluída, o banco de dados precisa verificar as restrições relacionais, ou seja, verifique se as relações são consistentes. Para fazer isso, o banco de dados precisa verificar as chaves estrangeiras nas tabelas relacionadas. Isso pode resultar na aquisição de outro bloqueio que não seja a linha modificada. Certifique-se de sempre ter índice nas chaves estrangeiras (e, é claro, nas chaves primárias), caso contrário, isso poderá resultar em um bloqueio de tabela em vez de um bloqueio de linha . Se o bloqueio da tabela ocorrer, a contenção do bloqueio é maior e a probabilidade de conflito aumenta.

ewernli
fonte
3
Então, talvez o meu problema seja que o usuário atualizou a página e, assim, acionou uma UPDATE de um registro ao mesmo tempo em que o cron está tentando executar um DELETE no registro. No entanto, estou recebendo o erro em INSERTS, para que o cron não seja DELETING registros que acabam de ser criados. Então, como pode ocorrer um impasse em um registro que ainda está para ser inserido?
David
Você pode fornecer um pouco mais de informações sobre as tabelas e o que as transações fazem exatamente?
ewernli
Não vejo como um impasse poderia acontecer se houver apenas uma instrução por transação. Nenhuma outra operação em outras tabelas? Não há chaves estrangeiras especiais ou restrições exclusivas? Não há restrições de exclusão em cascata?
ewernli
Não, nada mais especial ... Suponho que esteja relacionado à natureza do uso da tabela. uma linha está sendo inserida / atualizada a cada atualização de página de um visitante. Cerca de 1000 visitantes estão presentes a qualquer momento.
David
12

É provável que a instrução de exclusão afete uma grande fração do total de linhas na tabela. Eventualmente, isso pode levar à aquisição de um bloqueio de tabela ao excluir. Manter um bloqueio (nesse caso, bloqueios de linha ou de página) e adquirir mais bloqueios é sempre um risco de conflito. No entanto, não posso explicar por que a instrução insert leva a uma escalada de bloqueio - pode ter a ver com divisão / adição de páginas, mas alguém que conhece melhor o MySQL terá que preencher lá.

Para começar, pode valer a pena tentar adquirir explicitamente um bloqueio de tabela imediatamente para a instrução delete. Consulte TABELAS DE BLOQUEIO e problemas de bloqueio de tabela .

Anders Abel
fonte
6

Você pode tentar que esse deletetrabalho funcione inserindo primeiro a chave de cada linha a ser excluída em uma tabela temporária como este pseudocódigo

create temporary table deletetemp (userid int);

insert into deletetemp (userid)
  select userid from onlineusers where datetime <= now - interval 900 second;

delete from onlineusers where userid in (select userid from deletetemp);

Quebrar dessa maneira é menos eficiente, mas evita a necessidade de manter um bloqueio de intervalo de teclas durante o delete.

Além disso, modifique suas selectconsultas para adicionar uma wherecláusula excluindo linhas com mais de 900 segundos. Isso evita a dependência do trabalho cron e permite reagendá-lo para execução com menos frequência.

Teoria sobre os deadlocks: Eu não tenho muito conhecimento de fundo no MySQL, mas aqui vai ... Ele deletemanterá um bloqueio de intervalo de chaves por data e hora, para impedir que linhas correspondentes à sua wherecláusula sejam adicionadas no meio da transação e, ao encontrar linhas para excluir, tentará obter um bloqueio em cada página que está modificando. Ele insertadquirirá um bloqueio na página em que está inserindo e, em seguida, tentará adquirir o bloqueio de chave. Normalmente insert, esperará pacientemente a abertura da chave, mas isso trará um deletetravamento se tentar bloquear a mesma página que insertestá sendo usada, porque as deletenecessidades dessa página são bloqueadas e as insertque precisam ser bloqueadas. No entanto, isso não parece adequado para as inserções, deleteeinsert estão usando intervalos de data e hora que não se sobrepõem, então talvez algo mais esteja acontecendo.

http://dev.mysql.com/doc/refman/5.1/en/innodb-next-key-locking.html

Brian Sandlin
fonte
4

Caso alguém ainda esteja lutando com esse problema:

Eu enfrentei um problema semelhante, onde 2 pedidos estavam atingindo o servidor ao mesmo tempo. Não houve situação como abaixo:

T1:
    BEGIN TRANSACTION
    INSERT TABLE A
    INSERT TABLE B
    END TRANSACTION

T2:
    BEGIN TRANSACTION
    INSERT TABLE B
    INSERT TABLE A
    END TRANSACTION

Então, fiquei perplexo porque o impasse está acontecendo.

Então, descobri que havia relação de pai-filho entre duas tabelas por causa da chave estrangeira. Quando eu estava inserindo um registro na tabela filho, a transação estava adquirindo um bloqueio na linha da tabela pai. Imediatamente depois disso, eu estava tentando atualizar a linha pai, que estava acionando a elevação do bloqueio para uma EXCLUSIVA. Como a segunda transação simultânea já estava mantendo um bloqueio SHARED, estava causando um conflito.

Consulte: https://blog.tekenlight.com/2019/02/21/database-deadlock-mysql.html

chatsap
fonte
Também no meu caso, parece que o problema era uma relação de chave estrangeira. Obrigado1
Chris Prince
3

Para programadores Java que usam Spring, evitei esse problema usando um aspecto de AOP que tenta novamente automaticamente transações que são executadas em conflitos transitórios.

Consulte Javadoc @RetryTransaction para obter mais informações.

Archie
fonte
0

Eu tenho um método, cujos internos estão envolvidos em um MySqlTransaction.

O problema do impasse apareceu para mim quando eu executei o mesmo método em paralelo consigo mesmo.

Não houve problema ao executar uma única instância do método.

Quando removi o MySqlTransaction, consegui executar o método paralelamente, sem problemas.

Apenas compartilhando minha experiência, não estou defendendo nada.

CINCHAPPS
fonte
0

croné perigoso. Se uma instância do cron falhar ao terminar antes da próxima, é provável que elas se enfrentem.

Seria melhor ter um trabalho em execução contínua que excluísse algumas linhas, dormisse algumas e depois repetisse.

Além disso, INDEX(datetime)é muito importante para evitar conflitos.

Mas, se o teste de data e hora incluir mais de, digamos, 20% da tabela, DELETEele fará uma varredura na tabela. Pedaços menores excluídos com mais frequência são uma solução alternativa.

Outro motivo para usar blocos menores é bloquear menos linhas.

Bottom line:

  • INDEX(datetime)
  • Tarefa em execução contínua - exclua, durma um minuto, repita.
  • Para garantir que a tarefa acima não tenha terminado, tenha um trabalho cron cujo único objetivo seja reiniciá-lo em caso de falha.

Outras técnicas de exclusão: http://mysql.rjweb.org/doc.php/deletebig

Rick James
fonte