Definindo um limite de tempo para uma transação no MySQL / InnoDB

11

Isso surgiu a partir dessa pergunta relacionada , em que eu queria saber como forçar duas transações a ocorrerem seqüencialmente em um caso trivial (onde ambas estão operando em apenas uma única linha). Eu recebi uma resposta - use SELECT ... FOR UPDATEcomo a primeira linha de ambas as transações - mas isso leva a um problema: se a primeira transação nunca for confirmada ou revertida, a segunda transação será bloqueada indefinidamente. A innodb_lock_wait_timeoutvariável define o número de segundos após os quais o cliente que tenta fazer a segunda transação recebe a mensagem "Desculpe, tente novamente" ... mas, tanto quanto eu sei, eles tentariam novamente até o próximo servidor reiniciar. Então:

  1. Certamente deve haver uma maneira de forçar um ROLLBACKse uma transação estiver demorando para sempre? Devo recorrer ao uso de um daemon para eliminar essas transações? Em caso afirmativo, como seria esse daemon?
  2. Se uma conexão é interrompida por wait_timeoutou no interactive_timeoutmeio da transação, a transação é revertida? Existe uma maneira de testar isso no console?

Esclarecimento : innodb_lock_wait_timeoutdefine o número de segundos que uma transação aguardará que um bloqueio seja liberado antes de desistir; o que eu quero é uma maneira de forçar um bloqueio a ser liberado.

Atualização 1 : Aqui está um exemplo simples que demonstra por que innodb_lock_wait_timeoutnão é suficiente para garantir que a segunda transação não seja bloqueada pela primeira:

START TRANSACTION;
SELECT SLEEP(55);
COMMIT;

Com a configuração padrão de innodb_lock_wait_timeout = 50, esta transação é concluída sem erros após 55 segundos. E se você adicionar um UPDATEantes da SLEEPlinha, inicie uma segunda transação de outro cliente que tenta SELECT ... FOR UPDATEa mesma linha, é a segunda transação que atinge o tempo limite, não a que adormeceu.

O que estou procurando é uma maneira de forçar o fim do sono repousante dessa transação.

Atualização 2 : em resposta às preocupações da hobodave sobre o quão realista é o exemplo acima, aqui está um cenário alternativo: um DBA se conecta a um servidor ativo e executa

START TRANSACTION
SELECT ... FOR UPDATE

onde a segunda linha trava uma linha na qual o aplicativo grava frequentemente. Em seguida, o DBA é interrompido e se afasta, esquecendo de finalizar a transação. O aplicativo é interrompido até a linha ser desbloqueada. Gostaria de minimizar o tempo em que o aplicativo está bloqueado como resultado desse erro.

Trevor Burnham
fonte
Você acabou de atualizar sua pergunta para entrar em conflito com a sua pergunta inicial. Você declara em negrito "Se a primeira transação nunca for confirmada ou revertida, a segunda transação será bloqueada indefinidamente". Você nega isso com sua atualização mais recente: "é a segunda transação que atinge o tempo limite, não a que adormeceu". -- seriamente?!
hobodave
Suponho que meu fraseado poderia ter sido mais claro. Por "bloqueado indefinidamente", quis dizer que a segunda transação expiraria com uma mensagem de erro que dizia "tente reiniciar a transação"; mas fazê-lo seria infrutífero, porque seria interrompido novamente. Portanto, a segunda transação é bloqueada - ela nunca será concluída, porque continuará atingindo o tempo limite.
Trevor Burnham
@ Trevor: Seu fraseado era perfeitamente claro. Você simplesmente atualizou sua pergunta para fazer uma coisa completamente diferente, que é uma forma extremamente ruim.
precisa saber é o seguinte
A pergunta sempre foi a mesma: é um problema que a segunda transação seja bloqueada indefinidamente quando a primeira transação nunca é confirmada ou revertida. Quero forçar um ROLLBACKna primeira transação, se demorar mais de nalguns segundos para concluir. Existe alguma maneira de fazer isso?
Trevor Burnham
1
@TrevorBurnham Também me pergunto por que MYSQLnão tem uma configuração para evitar esse cenário. Porque não é aceitável a interrupção do servidor devido à irresponsabilidade dos clientes. Não encontrei nenhuma dificuldade para entender sua pergunta também é tão relevante.
Dinoop paloli

Respostas:

3

Como sua pergunta é feita aqui no ServerFault, é lógico supor que você esteja procurando uma solução MySQL para um problema do MySQL, particularmente no campo do conhecimento em que um administrador de sistema e / ou um DBA teriam experiência. Como tal, os seguintes A seção aborda suas perguntas:

Se a primeira transação nunca for confirmada ou revertida, a segunda transação será bloqueada indefinidamente

Não, não vai. Eu acho que você não está entendendo innodb_lock_wait_timeout. Faz exatamente o que você precisa.

Ele retornará com um erro, conforme indicado no manual:

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
  • Por definição, isso não é indefinido . Se sua aplicação for reconectar e bloqueando várias vezes, em seguida, sua aplicação é "bloquear indefinidamente", não a transação. A segunda transação é bloqueada definitivamente por innodb_lock_wait_timeoutsegundos.

Por padrão, a transação não será revertida. É de responsabilidade do código do aplicativo decidir como lidar com esse erro, seja tentando novamente ou revertendo.

Se você deseja uma reversão automática, isso também é explicado no manual:

A transação atual não é revertida. (Para reverter toda a transação, inicie o servidor com a --innodb_rollback_on_timeoutopção


RE: Suas inúmeras atualizações e comentários

Primeiro, você declarou em seus comentários que "pretendia" dizer que deseja uma maneira de atingir o tempo limite da primeira transação que está bloqueando indefinidamente. Isso não é aparente na sua pergunta original e entra em conflito com "Se a primeira transação nunca for confirmada ou revertida, a segunda transação será bloqueada indefinidamente".

No entanto, também posso responder a essa pergunta. O protocolo MySQL não possui um "tempo limite de consulta". Isso significa que você não pode atingir o tempo limite da primeira transação bloqueada. Você deve esperar até que esteja terminado ou matar a sessão. Quando a sessão é encerrada, o servidor reverte automaticamente a transação.

A única outra alternativa seria usar ou escrever uma biblioteca mysql que utiliza E / S sem bloqueio, o que permitiria que seu aplicativo eliminasse o thread / fork fazendo a consulta após N segundos. A implementação e o uso dessa biblioteca estão além do escopo do ServerFault. Esta é uma pergunta apropriada para StackOverflow .

Em segundo lugar, você declarou o seguinte em seus comentários:

Na verdade, eu estava mais preocupado na minha pergunta com um cenário em que o aplicativo cliente trava (digamos, fica preso em um loop infinito) durante o curso de uma transação do que com aquele em que a transação leva muito tempo no final do MySQL.

Isso não ficou aparente na sua pergunta original e ainda não é. Isso só pôde ser discernido depois que você compartilhou esse detalhe bastante importante no comentário.

Se esse é realmente o problema que você está tentando resolver, receio que você o tenha solicitado no fórum errado. Você descreveu um problema de programação no nível do aplicativo que requer uma solução de programação que o MySQL não pode fornecer e está fora do escopo desta comunidade. Sua resposta mais recente responde à pergunta "Como evito que um programa Ruby faça um loop infinito?". Essa pergunta é fora de tópico para esta comunidade e deve ser feita no StackOverflow .

hobodave
fonte
Desculpe, mas embora seja verdade quando a transação está aguardando um bloqueio (como o nome e a documentação da innodb_lock_wait_timeoutsugestão), não é assim na situação que eu coloquei: onde o cliente trava ou trava antes de fazer a alteração COMMITou ROLLBACK.
Trevor Burnham
@ Trevor: Você está simplesmente errado. Isso é trivial para testar do seu lado. Acabei de o testar. Retire o seu voto negativo.
precisa saber é o seguinte
@ hobo, só porque o seu direito não significa que ele precisa gostar.
Chris S
Chris, você poderia explicar como ele está certo? Como será a segunda transação ocorrer se nenhum COMMITou ROLLBACKmensagem é enviada para resolver a primeira transação? Hobodave, talvez seja útil se você apresentar seu código de teste.
Trevor Burnham
2
@ Trevor: Você não está fazendo um pouco de sentido. Você deseja serializar transações, certo? Sim, você disse isso em sua outra pergunta e repita isso aqui nesta pergunta. Se a primeira transação não foi concluída, como você espera que a segunda transação seja concluída? Você disse explicitamente para aguardar o bloqueio ser liberado nessa linha. Portanto, deve esperar. O que você não entende sobre isso?
precisa saber é o seguinte
1

Em uma situação semelhante, minha equipe decidiu usar pt-kill ( https://www.percona.com/doc/percona-toolkit/2.1/pt-kill.html ). Ele pode relatar ou eliminar consultas que (entre outras coisas) estão sendo executadas por muito tempo. É então possível executá-lo em um cron a cada X minutos.

O problema era que uma consulta que teve um tempo de execução inesperadamente longo (por exemplo, indefinido) bloqueou um procedimento de backup que tentou liberar tabelas com bloqueio de leitura, que, por sua vez, bloqueou todas as outras consultas sobre "aguardando a liberação da tabela", que nunca expirou. porque eles não estão aguardando um bloqueio, o que resultou em nenhum erro no aplicativo, o que resultou em nenhum alerta para os administradores e a interrupção do aplicativo.

A correção foi obviamente para corrigir a consulta inicial, mas para evitar que a situação aconteça acidentalmente no futuro, seria ótimo se fosse possível matar automaticamente (ou reportar) transações / consultas que duram mais do que o tempo específico, qual autor da pergunta parece estar perguntando. Isso é possível com pt-kill.

Krešimir Nesek
fonte
0

Uma solução simples será ter um procedimento armazenado para eliminar as consultas que levam mais tempo que o timeouttempo necessário e usar a innodb_rollback_on_timeoutopção junto com ele.

Sony Mathew
fonte
-2

Depois de pesquisar no Google por um tempo, parece que não há maneira direta de definir um limite de tempo por transação. Aqui está a melhor solução que consegui encontrar:

O comando

SHOW PROCESSLIST;

fornece uma tabela com todos os comandos em execução no momento e o tempo (em segundos) desde que foram iniciados. Quando um cliente para de dar comandos, eles são descritos como dando um Sleepcomando, com a Timecoluna sendo o número de segundos desde que o último comando foi concluído. Portanto, você pode entrar manualmente e KILLtudo o que tiver um Timevalor acima de, digamos, 5 segundos, se tiver certeza de que nenhuma consulta deve levar mais de 5 segundos no banco de dados. Por exemplo, se o processo com o ID 3 tiver um valor 12 na Timecoluna, você poderá fazer

KILL 3;

A documentação para a sintaxe KILL sugere que uma transação sendo executada pelo encadeamento com esse ID seria revertida (embora eu não tenha testado isso, tenha cuidado).

Mas como automatizar isso e eliminar todos os scripts de horas extras a cada 5 segundos? O primeiro comentário nessa mesma página nos dá uma dica, mas o código está em PHP; parece que não podemos fazer um SELECTon SHOW PROCESSLISTde dentro do cliente MySQL. Ainda assim, supondo que tenhamos um daemon PHP que executamos a cada $MAX_TIMEsegundo, pode ser algo como isto:

$result = mysql_query("SHOW FULL PROCESSLIST");
while ($row=mysql_fetch_array($result)) {
  $process_id=$row["Id"];
    if ($row["Time"] > $MAX_TIME) {
      $sql="KILL $process_id";
      mysql_query($sql);
  }
}

Observe que isso teria o efeito colateral de forçar a conexão de todas as conexões ociosas do cliente, não apenas aquelas que estão se comportando mal.

Se alguém tiver uma resposta melhor, eu adoraria ouvi-la.

Edit: Existe pelo menos um risco sério de usar KILLdessa maneira. Suponha que você use o script acima para KILLtodas as conexões inativas por 10 segundos. Após uma conexão KILLficar inativa por 10 segundos, mas antes da emissão, o usuário cuja conexão está sendo encerrada emite um START TRANSACTIONcomando. Eles são então KILLed. Eles enviam um UPDATEe, pensando duas vezes, emitem a ROLLBACK. No entanto, como a KILLreversão da transação original foi revertida, a atualização foi realizada imediatamente e não pode ser revertida! Veja esta postagem para um caso de teste.

Trevor Burnham
fonte
2
Este comentário é destinado a futuros leitores desta resposta. Por favor, não faça isso, mesmo que seja a resposta aceita.
precisa saber é o seguinte
OK, em vez de diminuir o voto e deixar um comentário malicioso, que tal explicar por que isso seria uma má idéia? A pessoa que postou o script PHP original disse que impedia o servidor de travar; se houver uma maneira melhor de alcançar o mesmo objetivo, mostre-me.
Trevor Burnham
6
O comentário não é malicioso. É um aviso para todos os futuros leitores não tentarem usar sua "solução". Matar automaticamente transações de longa duração é uma ideia unilateralmente terrível. Isso nunca deve ser feito . Essa é a explicação. As sessões de assassinato devem ser uma exceção, não uma norma. A causa raiz do seu problema artificial é erro do usuário. Você não cria soluções técnicas para DBAs que bloqueiam linhas e depois "vão embora". Você os despede.
precisa saber é o seguinte
-2

Como hobodave sugeriu em um comentário, é possível em alguns clientes (embora não seja, aparentemente, o mysqlutilitário de linha de comando) definir um limite de tempo para uma transação. Aqui está uma demonstração usando o ActiveRecord no Ruby:

require 'rubygems'
require 'timeout'
require 'active_record'

Timeout::timeout(5) {
  Foo.transaction do
    Foo.create(:name => 'Bar')
    sleep 10
  end
}

Neste exemplo, a transação atinge o tempo limite após 5 segundos e é revertida automaticamente . ( Atualize em resposta ao comentário do hobodave: se o banco de dados levar mais de 5 segundos para responder, a transação será revertida assim que acontecer - e não antes). Se você deseja garantir que todas as suas transações expirem após nsegundos, você pode criar um wrapper em torno do ActiveRecord. Suponho que isso também se aplique às bibliotecas mais populares em Java, .NET, Python, etc., mas ainda não as testei. (Se tiver, poste um comentário sobre esta resposta.)

As transações no ActiveRecord também têm a vantagem de serem seguras se uma KILLé emitida, ao contrário das transações feitas na linha de comando. Consulte /dba/1561/mysql-client-believes-theyre-in-a-transaction-gets-killed-wreaks-havoc .

Não parece possível impor um tempo máximo de transação no servidor, exceto por meio de um script como o que eu publiquei na minha outra resposta.

Trevor Burnham
fonte
Você ignorou que o ActiveRecord usa E / S síncrona (bloqueadora). Tudo o que esse script está fazendo é interromper o comando Ruby sleep. Se o seu Foo.createcomando bloquear por 5 minutos, o Timeout não o matará. Isso é incompatível com o exemplo fornecido na sua pergunta. A SELECT ... FOR UPDATEpoderia bloquear facilmente por longos períodos de tempo, como qualquer outra consulta.
hobodave
Deve-se notar que quase todas as bibliotecas cliente mysql utilizam E / S de bloqueio, daí a sugestão na minha resposta de que você precisaria usar ou escrever sua própria E / S sem bloqueio. Aqui está uma demonstração simples de que o Timeout é inútil: gist.github.com/856100
hobodave
Na verdade, eu estava mais preocupado na minha pergunta com um cenário em que o aplicativo cliente trava (digamos, fica preso em um loop infinito) durante o curso de uma transação do que com aquele em que a transação leva muito tempo no final do MySQL. Mas obrigado, esse é um bom argumento. Eu atualizei a questão notar-lo).
Trevor Burnham
Não consigo duplicar os resultados reivindicados. Atualizei o gist para utilizar ActiveRecord::Base#find_by_sql: gist.github.com/856100 Novamente, isso ocorre porque o AR usa bloqueio de E / S.
precisa saber é o seguinte
@hobodave Sim, essa edição foi incorreta e foi corrigida. Para ser claro: o timeoutmétodo reverterá a transação, mas não até que o banco de dados MySQL responda a uma consulta. Portanto, o timeoutmétodo é adequado para evitar transações longas no lado do cliente ou transações longas que consistem em várias consultas relativamente curtas, mas não para transações longas nas quais uma única consulta é a culpada.
Trevor Burnham