Establecer un límite de tiempo para una transacción en MySQL / InnoDB

11

Esto surgió de esta pregunta relacionada , donde quería saber cómo forzar que dos transacciones ocurrieran secuencialmente en un caso trivial (donde ambas operan en una sola fila). Obtuve una respuesta, usar SELECT ... FOR UPDATEcomo la primera línea de ambas transacciones, pero esto lleva a un problema: si la primera transacción nunca se confirma o revierte, la segunda transacción se bloqueará indefinidamente. La innodb_lock_wait_timeoutvariable establece el número de segundos después de los cuales el cliente que intenta hacer la segunda transacción se le dice "Lo siento, inténtelo de nuevo" ... pero hasta donde puedo decir, lo intentarían nuevamente hasta el próximo reinicio del servidor. Entonces:

  1. ¿Seguramente debe haber una manera de forzar un ROLLBACKsi una transacción está tomando para siempre? ¿Debo recurrir al uso de un demonio para matar tales transacciones, y si es así, cómo sería ese demonio?
  2. Si la conexión es matado por wait_timeouto interactive_timeoutmediados de transacción, la transacción se deshace? ¿Hay alguna forma de probar esto desde la consola?

Aclaración : innodb_lock_wait_timeoutestablece el número de segundos que una transacción esperará a que se libere un bloqueo antes de darse por vencido; lo que quiero es una forma de forzar la liberación de un bloqueo.

Actualización 1 : Aquí hay un ejemplo simple que demuestra por qué innodb_lock_wait_timeoutno es suficiente para garantizar que la segunda transacción no esté bloqueada por la primera:

START TRANSACTION;
SELECT SLEEP(55);
COMMIT;

Con la configuración predeterminada de innodb_lock_wait_timeout = 50, esta transacción se completa sin errores después de 55 segundos. Y si agrega un UPDATEantes de la SLEEPlínea, luego inicia una segunda transacción desde otro cliente que intenta en SELECT ... FOR UPDATEla misma fila, es la segunda transacción que agota el tiempo de espera, no la que se durmió.

Lo que estoy buscando es una forma de forzar el final del sueño reparador de esta transacción.

Actualización 2 : en respuesta a las preocupaciones de hobodave sobre cuán realista es el ejemplo anterior, aquí hay un escenario alternativo: un DBA se conecta a un servidor en vivo y se ejecuta

START TRANSACTION
SELECT ... FOR UPDATE

donde la segunda línea bloquea una fila en la que la aplicación escribe con frecuencia. Luego, el DBA se interrumpe y se va, olvidando finalizar la transacción. La aplicación se detiene hasta que se desbloquea la fila. Me gustaría minimizar el tiempo que la aplicación está atascada como resultado de este error.

Trevor Burnham
fuente
Acaba de actualizar su pregunta para entrar en conflicto con su pregunta inicial. Usted declara en negrita "Si la primera transacción nunca se confirma o revierte, entonces la segunda transacción se bloqueará indefinidamente". Usted refuta esto con su última actualización: "es la segunda transacción que agota el tiempo de espera, no la que se durmió". -- ¡¿seriamente?!
hobodave
Supongo que mi fraseo podría haber sido más claro. Por "bloqueado indefinidamente", quise decir que la segunda transacción expiraría con un mensaje de error que dice "intente reiniciar la transacción"; pero hacerlo sería infructuoso, porque volvería a pasar el tiempo. Por lo tanto, la segunda transacción está bloqueada, nunca se completará, porque seguirá agotando el tiempo.
Trevor Burnham
@Trevor: Tu fraseo fue perfectamente claro. Simplemente has actualizado tu pregunta para hacer una pregunta completamente diferente, que es una forma extremadamente mala.
hobodave
La pregunta siempre ha sido la misma: es un problema que la segunda transacción se bloquee indefinidamente cuando la primera transacción nunca se confirma o revierte. Quiero forzar un ROLLBACKen la primera transacción si tarda más de nsegundos en completarse. ¿Hay alguna manera de hacerlo?
Trevor Burnham
1
@TrevorBurnham También me pregunto por qué no MYSQLtiene una configuración para evitar este escenario. Porque no es aceptable que el servidor se cuelgue debido a la irresponsabilidad de los clientes. No encontré ninguna dificultad para entender su pregunta, también es tan relevante.
Dinoop paloli

Respuestas:

3

Dado que su pregunta se hace aquí en ServerFault, es lógico suponer que está buscando una solución MySQL para un problema MySQL, particularmente en el ámbito del conocimiento en el que un administrador de sistemas y / o un DBA tendrían experiencia. Como tal, lo siguiente La sección aborda sus preguntas:

Si la primera transacción nunca se confirma o revierte, la segunda transacción se bloqueará indefinidamente

No, no lo hará. Creo que no estás entendiendo innodb_lock_wait_timeout. Hace exactamente lo que necesitas.

Volverá con un error como se indica en el manual:

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
  • Por definición, esto no es indefinido . Si su aplicación se está volviendo a conectar y está bloqueando repetidamente, entonces su aplicación está "bloqueándose indefinidamente", no la transacción. La segunda transacción se bloquea muy definitivamente por innodb_lock_wait_timeoutsegundos.

Por defecto, la transacción no se revertirá. Es responsabilidad de su código de aplicación decidir cómo manejar este error, ya sea que lo intente nuevamente o retroceda.

Si desea una reversión automática, eso también se explica en el manual:

La transacción actual no se revierte. (Para que se revierta toda la transacción, inicie el servidor con la --innodb_rollback_on_timeoutopción.


RE: sus numerosas actualizaciones y comentarios

Primero, usted ha declarado en sus comentarios que "quiso" decir que desea una forma de expirar la primera transacción que se está bloqueando indefinidamente. Esto no se desprende de su pregunta original y entra en conflicto con "Si la primera transacción nunca se confirma o revierte, entonces la segunda transacción se bloqueará indefinidamente".

No obstante, también puedo responder esa pregunta. El protocolo MySQL no tiene un "tiempo de espera de consulta". Esto significa que no puede agotar el tiempo de espera de la primera transacción bloqueada. Debe esperar hasta que termine o cerrar la sesión. Cuando finaliza la sesión, el servidor revierte automáticamente la transacción.

La única otra alternativa sería usar o escribir una biblioteca mysql que utilice E / S sin bloqueo que permitiría que su aplicación elimine el hilo / tenedor que realiza la consulta después de N segundos. La implementación y el uso de dicha biblioteca están más allá del alcance de ServerFault. Esta es una pregunta apropiada para StackOverflow .

En segundo lugar, has indicado lo siguiente en tus comentarios:

En realidad, estaba más preocupado en mi pregunta con un escenario en el que la aplicación del cliente se cuelga (por ejemplo, queda atrapada en un bucle infinito) durante el curso de una transacción que con uno en el que la transacción lleva mucho tiempo al final de MySQL.

Esto no fue del todo aparente en su pregunta original, y aún no lo es. Esto solo se pudo discernir después de que compartió este dato bastante importante en el comentario.

Si este es realmente el problema que está tratando de resolver, me temo que lo ha preguntado en el foro equivocado. Usted ha descrito un problema de programación a nivel de aplicación que requiere una solución de programación, una que MySQL no puede proporcionar, y está fuera del alcance de esta comunidad. Su última respuesta responde a la pregunta "¿Cómo evito que un programa Ruby se repita infinitamente?". Esa pregunta está fuera de tema para esta comunidad y debe formularse en StackOverflow .

hobodave
fuente
Lo sentimos, pero si bien es cierto cuando la transacción está esperando un bloqueo (como lo innodb_lock_wait_timeoutsugieren el nombre y la documentación ), no es así en la situación que planteé: donde el cliente se cuelga o falla antes de obtener el cambio para hacer un COMMITo ROLLBACK.
Trevor Burnham
@Trevor: Simplemente estás equivocado. Esto es trivial para probar en tu extremo. Acabo de probarlo de mi parte. Por favor, retire su voto negativo.
hobodave
@hobo, solo porque tu derecho no significa que tiene que gustarle.
Chris S
Chris, ¿podrías explicar cómo tiene razón? ¿Cómo se producirá la segunda transacción si hay COMMITo ROLLBACKse envía el mensaje a resolver la primera transacción? Hobodave, quizás sería útil si presentaras tu código de prueba.
Trevor Burnham
2
@Trevor: No tienes sentido. Quiere serializar transacciones, ¿verdad? Sí, lo dijiste en tu otra pregunta y repite eso aquí en esta pregunta. Si la primera transacción no se ha completado, ¿cómo esperaría que se complete la segunda transacción? Le ha dicho explícitamente que espere a que se libere el bloqueo en esa fila. Por lo tanto, debe esperar. ¿Qué no entiendes sobre esto?
hobodave
1

En una situación similar, mi equipo decidió usar pt-kill ( https://www.percona.com/doc/percona-toolkit/2.1/pt-kill.html ). Puede informar o eliminar consultas que (entre otras cosas) se ejecutan durante demasiado tiempo. Entonces es posible ejecutarlo en un cron cada X minutos.

El problema era que una consulta que tenía un tiempo de ejecución inesperadamente largo (por ejemplo, indefinido) bloqueó un procedimiento de copia de seguridad que trató de eliminar las tablas con el bloqueo de lectura, que a su vez bloqueó todas las demás consultas sobre "esperar a que las tablas se vacíen", que nunca expiraron porque no están esperando un bloqueo, lo que resultó sin errores en la aplicación, lo que finalmente resultó sin alertas a los administradores y la detención de la aplicación.

Obviamente, la solución era solucionar la consulta inicial, pero para evitar que la situación ocurriera accidentalmente en el futuro, sería genial si fuera posible eliminar automáticamente (o informar) transacciones / consultas que duran más de un tiempo específico, qué autor de la pregunta parece estar preguntando. Esto es factible con pt-kill.

Krešimir Nesek
fuente
0

Una solución simple será tener un procedimiento almacenado para eliminar las consultas que toman más tiempo que el timeouttiempo requerido y usar la innodb_rollback_on_timeoutopción junto con él.

Sony Mathew
fuente
-2

Después de buscar en Google por un tiempo, parece que no hay una forma directa de establecer un límite de tiempo por transacción. Aquí está la mejor solución que pude encontrar:

El comando

SHOW PROCESSLIST;

le proporciona una tabla con todos los comandos que se ejecutan actualmente y el tiempo (en segundos) desde que comenzaron. Cuando un cliente ha dejado de dar comandos, se describe como dar un Sleepcomando, y la Timecolumna es el número de segundos desde que se completó su último comando. Por lo tanto, podría ingresar manualmente y KILLtodo lo que tenga un Timevalor superior a, por ejemplo, 5 segundos si está seguro de que ninguna consulta debería tomar más de 5 segundos en su base de datos. Por ejemplo, si el proceso con id 3 tiene un valor de 12 en su Timecolumna, podría hacer

KILL 3;

La documentación para la sintaxis KILL sugiere que una transacción que se está llevando a cabo por el subproceso con esa identificación se revertiría (aunque no lo he probado, así que tenga cuidado).

Pero, ¿cómo automatizar esto y eliminar todos los scripts de tiempo extra cada 5 segundos? El primer comentario en esa misma página nos da una pista, pero el código está en PHP; parece que no podemos hacer un SELECTencendido SHOW PROCESSLISTdesde el cliente MySQL. Aún así, suponiendo que tengamos un demonio PHP que ejecutamos cada $MAX_TIMEsegundo, podría verse así:

$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);
  }
}

Tenga en cuenta que esto tendría el efecto secundario de forzar la reconexión de todas las conexiones inactivas del cliente, no solo aquellas que se comportan mal.

Si alguien tiene una mejor respuesta, me encantaría escucharla.

Editar: hay al menos un riesgo grave de usar KILLde esta manera. Suponga que usa el script anterior para KILLcada conexión que esté inactiva durante 10 segundos. Después de que una conexión ha estado inactiva durante 10 segundos, pero antes de que KILLse emita, el usuario cuya conexión se está desconectando emite un START TRANSACTIONcomando. Luego son KILLed. Envían un UPDATEy luego, pensando dos veces, emiten un ROLLBACK. Sin embargo, debido a que KILLrevertieron su transacción original, la actualización se realizó de inmediato y no se puede revertir. Vea esta publicación para un caso de prueba.

Trevor Burnham
fuente
2
Este comentario está destinado a futuros lectores de esta respuesta. No hagas esto, incluso si es la respuesta aceptada.
hobodave
De acuerdo, en lugar de votar negativamente y dejar un comentario sarcástico, ¿qué tal explicar por qué sería una mala idea? La persona que publicó el script PHP original dijo que evitaba que su servidor se bloqueara; si hay una mejor manera de lograr el mismo objetivo, muéstrame.
Trevor Burnham
66
El comentario no es sarcástico. Es una advertencia para todos los futuros lectores que no intenten utilizar su "solución". Matar automáticamente las transacciones de larga duración es una idea unilateralmente terrible. Debe nunca se puede hacer . Esa es la explicación. Las sesiones de asesinato deberían ser una excepción, no una norma. La causa raíz de su problema artificial es el error del usuario. No crea soluciones técnicas para los DBA que bloquean filas y luego "se van". Tú los despides.
hobodave
-2

Como hobodave sugirió en un comentario, es posible en algunos clientes (aunque no, aparentemente, la mysqlutilidad de línea de comandos) establecer un límite de tiempo para una transacción. Aquí hay una demostración usando ActiveRecord en Ruby:

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

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

En este ejemplo, la transacción agota el tiempo de espera después de 5 segundos y se revierte automáticamente . ( Actualice en respuesta al comentario de hobodave: si la base de datos tarda más de 5 segundos en responder, la transacción se revertirá tan pronto como lo haga, no antes). Si desea asegurarse de que todas sus transacciones expiren después de nsegundos, podrías construir un contenedor alrededor de ActiveRecord. Supongo que esto también se aplica a las bibliotecas más populares en Java, .NET, Python, etc., pero aún no las he probado. (Si es así, publique un comentario sobre esta respuesta).

Las transacciones en ActiveRecord también tienen la ventaja de ser seguras si KILLse emite un, a diferencia de las transacciones realizadas desde la línea de comandos. Ver /dba/1561/mysql-client-believes-theyre-in-a-transaction-gets-killed-wreaks-havoc .

No parece posible imponer un tiempo máximo de transacción en el lado del servidor, excepto a través de un script como el que publiqué en mi otra respuesta.

Trevor Burnham
fuente
Has pasado por alto que ActiveRecord usa E / S síncrona (bloqueo). Todo lo que está haciendo el script es interrumpir el comando de suspensión de Ruby. Si su Foo.createcomando se bloqueó durante 5 minutos, el tiempo de espera no lo mataría. Esto es incompatible con el ejemplo proporcionado en su pregunta. A SELECT ... FOR UPDATEpodría bloquearse fácilmente durante largos períodos de tiempo, como cualquier otra consulta.
hobodave
Cabe señalar que casi todas las bibliotecas de clientes mysql utilizan E / S de bloqueo, de ahí la sugerencia en mi respuesta de que necesitaría usar o escribir las suyas que usaban E / S sin bloqueo. Aquí hay una demostración simple de que el Timeout es inútil: gist.github.com/856100
hobodave
En realidad, estaba más preocupado en mi pregunta con un escenario en el que la aplicación del cliente se cuelga (por ejemplo, queda atrapada en un bucle infinito) durante el curso de una transacción que con uno en el que la transacción lleva mucho tiempo al final de MySQL. Pero gracias, ese es un buen punto. He actualizado la pregunta para anotarla.)
Trevor Burnham
No puedo duplicar sus resultados reclamados. Actualicé la esencia para utilizar ActiveRecord::Base#find_by_sql: gist.github.com/856100 Nuevamente, esto se debe a que AR usa el bloqueo de E / S.
hobodave
@hobodave Sí, esa edición fue errónea y se ha corregido. Para ser claros: el timeoutmétodo revertirá la transacción, pero no hasta que la base de datos MySQL responda a una consulta. Por lo tanto, el timeoutmétodo es adecuado para evitar transacciones largas en el lado del cliente o transacciones largas que consisten en varias consultas relativamente cortas, pero no para transacciones largas en las que una sola consulta es la culpable.
Trevor Burnham