Cómo evitar que se encuentre mysql 'Deadlock al intentar bloquear; intente reiniciar la transacción '

286

Tengo una tabla innoDB que registra a los usuarios en línea. El usuario lo actualiza en cada actualización de la página para realizar un seguimiento de las páginas en las que se encuentra y su última fecha de acceso al sitio. Luego tengo un cron que se ejecuta cada 15 minutos para ELIMINAR registros antiguos.

Obtuve un 'Deadlock encontrado cuando trataba de obtener el bloqueo; intente reiniciar la transacción 'durante unos 5 minutos anoche y parece ser cuando ejecuta INSERT en esta tabla. ¿Alguien puede sugerir cómo evitar este error?

=== EDITAR ===

Estas son las consultas que se están ejecutando:

Primera visita al sitio:

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

En cada página de actualización:

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

Cron cada 15 minutos:

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

Luego hace algunos recuentos para registrar algunas estadísticas (es decir: miembros en línea, visitantes en línea).

David
fuente
¿Puede proporcionar más detalles sobre la estructura de la tabla? ¿Hay índices agrupados o no agrupados?
Anders Abel
13
dev.mysql.com/doc/refman/5.1/en/innodb-deadlocks.html - Ejecutar "show engine innodb status" proporcionaría diagnósticos útiles.
Martin
No es una buena práctica escribir una base de datos síncrona cuando los usuarios pisan una página. la forma correcta de hacerlo es mantenerlo en la memoria, como memcache o alguna cola rápida, y escribir en db con cron.
Nir

Respuestas:

292

Un truco fácil que puede ayudar con la mayoría de los puntos muertos es ordenar las operaciones en un orden específico.

Obtiene un punto muerto cuando dos transacciones intentan bloquear dos bloqueos en órdenes opuestas, es decir:

  • conexión 1: tecla de bloqueo (1), tecla de bloqueo (2);
  • conexión 2: llave de bloqueo (2), llave de bloqueo (1);

Si ambas se ejecutan al mismo tiempo, la conexión 1 bloqueará la llave (1), la conexión 2 bloqueará la llave (2) y cada conexión esperará a que la otra libere la llave -> punto muerto.

Ahora, si cambió sus consultas de modo que las conexiones bloquearían las claves en el mismo orden, es decir:

  • conexión 1: tecla de bloqueo (1), tecla de bloqueo (2);
  • conexión 2: llave de bloqueo ( 1 ), llave de bloqueo ( 2 );

Será imposible llegar a un punto muerto.

Entonces esto es lo que sugiero:

  1. Asegúrese de no tener otras consultas que bloqueen el acceso a más de una clave a la vez, excepto la declaración de eliminación. si lo hace (y sospecho que lo hace), ordene su DÓNDE en (k1, k2, .. kn) en orden ascendente.

  2. Arregle su declaración de eliminación para que funcione en orden ascendente:

Cambio

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

A

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

Otra cosa a tener en cuenta es que la documentación de mysql sugiere que en caso de un punto muerto, el cliente debería volver a intentarlo automáticamente. Puede agregar esta lógica a su código de cliente. (Digamos, 3 reintentos en este error en particular antes de darse por vencido).

Omry Yadan
fuente
2
Si tengo Transaction (autocommit = false), se produce una excepción de punto muerto. ¿Es suficiente volver a intentar la misma instrucción.executeUpdate () o toda la transacción ahora se recorta y debería revertirse + volver a ejecutar todo lo que se estaba ejecutando en ella?
Whome
55
Si tiene transacciones habilitadas, es todo o nada. Si tuvo una excepción de cualquier tipo, se garantiza que toda la transacción no tuvo ningún efecto. en ese caso, querrás reiniciar todo.
Omry Yadan
44
Una eliminación basada en una selección en una tabla enorme es mucho más lenta que una simple eliminación
Thermech
3
Muchas gracias amigo. El consejo 'ordenar declaraciones' solucionó mis problemas de bloqueo muerto.
Miere
44
@OmryYadan Por lo que sé, en MySQL no puede seleccionar en una subconsulta de la misma tabla en la que está realizando la ACTUALIZACIÓN. dev.mysql.com/doc/refman/5.7/en/update.html
artaxerxe
72

El punto muerto ocurre cuando dos transacciones se esperan para adquirir un bloqueo. Ejemplo:

  • Tx 1: bloqueo A, luego B
  • Tx 2: bloqueo B, luego A

Existen numerosas preguntas y respuestas sobre puntos muertos. Cada vez que inserta / actualiza / o elimina una fila, se adquiere un bloqueo. Para evitar un punto muerto, debe asegurarse de que las transacciones simultáneas no actualicen la fila en un orden que podría resultar en un punto muerto. En términos generales, intente adquirir el bloqueo siempre en el mismo orden, incluso en diferentes transacciones (por ejemplo, siempre la tabla A primero, luego la tabla B).

Otra razón para el punto muerto en la base de datos puede ser la falta de índices . Cuando se inserta / actualiza / elimina una fila, la base de datos debe verificar las restricciones relacionales, es decir, asegurarse de que las relaciones sean consistentes. Para hacerlo, la base de datos debe verificar las claves externas en las tablas relacionadas. Es posible que se obtenga otro bloqueo que no sea la fila que se modifica. Asegúrese de tener siempre un índice en las claves externas (y, por supuesto, las claves primarias), de lo contrario podría resultar en un bloqueo de tabla en lugar de un bloqueo de fila . Si se produce el bloqueo de la tabla, la contención del bloqueo es mayor y aumenta la probabilidad de un punto muerto.

ewernli
fuente
3
Entonces, tal vez mi problema es que el usuario ha actualizado la página y, por lo tanto, desencadena una ACTUALIZACIÓN de un registro al mismo tiempo que el cron está tratando de ejecutar DELETE en el registro. Sin embargo, obtengo el error en INSERTS, por lo que el cron no eliminaría los registros que se acaban de crear. Entonces, ¿cómo puede suceder un punto muerto en un registro que aún no se ha insertado?
David
¿Puede proporcionar un poco más de información sobre las tablas y lo que hacen exactamente las transacciones?
ewernli
No veo cómo podría ocurrir un punto muerto si solo hay una declaración por transacción. ¿Ninguna otra operación en otras mesas? ¿Sin claves foráneas especiales o restricciones únicas? ¿Sin restricciones de eliminación en cascada?
ewernli
no, nada más especial ... Supongo que se debe a la naturaleza del uso de la tabla. se inserta / actualiza una fila cada actualización de página de un visitante. Alrededor de más de 1000 visitantes están en cualquier momento.
David
12

Es probable que la instrucción delete afecte a una gran fracción del total de filas en la tabla. Eventualmente, esto podría llevar a que se adquiera un bloqueo de tabla al eliminar. Mantener un bloqueo (en este caso bloqueos de fila o página) y adquirir más bloqueos siempre es un riesgo de punto muerto. Sin embargo, no puedo explicar por qué la declaración de inserción conduce a una escalada de bloqueo: podría tener que ver con la división / adición de páginas, pero alguien que conozca MySQL mejor tendrá que completarla.

Para empezar, puede valer la pena tratar de adquirir explícitamente un bloqueo de tabla de inmediato para la instrucción delete. Ver LOCK TABLES y Table bloqueo de problemas .

Anders Abel
fuente
6

Puede intentar hacer deletefuncionar ese trabajo insertando primero la clave de cada fila que se eliminará en una tabla temporal 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);

Romperlo así es menos eficiente pero evita la necesidad de mantener un bloqueo de rango de teclas durante el delete.

Además, modifique sus selectconsultas para agregar una wherecláusula que excluya las filas anteriores a 900 segundos. Esto evita la dependencia del trabajo cron y le permite reprogramar que se ejecute con menos frecuencia.

Teoría sobre los puntos muertos: no tengo muchos antecedentes en MySQL, pero aquí va ... deleteMantendrá un bloqueo de rango de teclas para fecha y hora, para evitar whereque se agreguen filas que coincidan con su cláusula en el medio de la transacción y, a medida que encuentre filas para eliminar, intentará adquirir un bloqueo en cada página que está modificando. El insertva a adquirir un bloqueo en la página en la que se está insertando, y luego intentará adquirir el bloqueo de teclas. Normalmente insert, esperará pacientemente a que se abra ese bloqueo de teclas, pero esto se deletebloqueará si intenta bloquear la misma página que insertestá utilizando porque deletenecesita ese bloqueo de página y insertnecesita ese bloqueo de tecla. Sin embargo, esto no parece adecuado para los insertos, el deleteyinsert están utilizando rangos de fecha y hora que no se superponen, por lo que tal vez esté sucediendo algo más.

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

Brian Sandlin
fuente
4

En caso de que alguien todavía tenga problemas con este problema:

Me enfrenté a un problema similar en el que 2 solicitudes llegaban al servidor al mismo tiempo. No hubo ninguna situación como la siguiente:

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

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

Entonces, me sorprendió por qué está ocurriendo un punto muerto.

Luego descubrí que había una relación padre-hijo entre 2 tablas debido a la clave externa. Cuando estaba insertando un registro en la tabla secundaria, la transacción estaba adquiriendo un bloqueo en la fila de la tabla primaria. Inmediatamente después de eso, estaba tratando de actualizar la fila principal que estaba activando la elevación del bloqueo a EXCLUSIVO. Como la segunda transacción concurrente ya tenía un bloqueo COMPARTIDO, estaba causando un punto muerto.

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

chatsap
fuente
En mi caso también, parece que el problema era una relación de clave externa. Gracias1
Chris Prince
3

Para los programadores de Java que usan Spring, he evitado este problema usando un aspecto AOP que reintenta automáticamente las transacciones que se encuentran en puntos muertos transitorios.

Consulte @RetryTransaction Javadoc para obtener más información.

Archie
fuente
0

Tengo un método, cuyas partes internas están envueltas en una MySqlTransaction.

El problema del punto muerto se me apareció cuando ejecuté el mismo método en paralelo consigo mismo.

No hubo un problema al ejecutar una sola instancia del método.

Cuando eliminé MySqlTransaction, pude ejecutar el método en paralelo consigo mismo sin problemas.

Solo compartiendo mi experiencia, no estoy abogando por nada.

CINCHAPPS
fuente
0

crones peligroso. Si una instancia de cron no termina antes de la próxima, es probable que luchen entre sí.

Sería mejor tener un trabajo en ejecución continua que eliminaría algunas filas, suspendería algunas y luego repetiría.

Además, INDEX(datetime)es muy importante para evitar puntos muertos.

Pero, si la prueba de fecha y hora incluye más de, digamos, el 20% de la tabla, DELETErealizará un escaneo de la tabla. Los trozos más pequeños eliminados con más frecuencia son una solución alternativa.

Otra razón para ir con trozos más pequeños es bloquear menos filas.

Línea de fondo:

  • INDEX(datetime)
  • Ejecución continua de tareas: eliminar, dormir un minuto, repetir
  • Para asegurarse de que la tarea anterior no haya muerto, tenga un trabajo cron cuyo único propósito es reiniciarlo en caso de falla.

Otras técnicas de eliminación: http://mysql.rjweb.org/doc.php/deletebig

Rick James
fuente