MySQL InnoDB bloquea la clave principal al eliminar incluso en READ COMMITTED

11

Prefacio

Nuestra aplicación ejecuta varios hilos que ejecutan DELETEconsultas en paralelo. Las consultas afectan a los datos aislados, es decir, no debería existir la posibilidad de que concurran DELETEen las mismas filas de subprocesos separados. Sin embargo, según la documentación, MySQL usa el llamado bloqueo de 'siguiente clave' para las DELETEdeclaraciones, que bloquea tanto la clave coincidente como alguna brecha. Esto lleva a puntos muertos y la única solución que hemos encontrado es usar el READ COMMITTEDnivel de aislamiento.

El problema

El problema surge cuando se ejecutan DELETEdeclaraciones complejas con JOINs de tablas enormes. En un caso particular, tenemos una tabla con advertencias que tiene solo dos filas, pero la consulta debe eliminar todas las advertencias que pertenecen a algunas entidades particulares de dos INNER JOINtablas ed separadas . La consulta es la siguiente:

DELETE pw 
FROM proc_warnings pw 
INNER JOIN day_position dp 
   ON dp.transaction_id = pw.transaction_id 
INNER JOIN ivehicle_days vd 
   ON vd.id = dp.ivehicle_day_id 
WHERE vd.ivehicle_id=? AND dp.dirty_data=1

Cuando la tabla day_position es lo suficientemente grande (en mi caso de prueba hay 1448 filas), cualquier transacción, incluso con el READ COMMITTEDmodo de aislamiento, bloquea toda la proc_warnings tabla.

El problema siempre se reproduce en estos datos de muestra: http://yadi.sk/d/QDuwBtpW1BxB9 tanto en MySQL 5.1 (verificado en 5.1.59) como en MySQL 5.5 (verificado en MySQL 5.5.24).

EDITAR: Los datos de muestra vinculados también contienen esquemas e índices para las tablas de consulta, reproducidos aquí por conveniencia:

CREATE TABLE  `proc_warnings` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `transaction_id` int(10) unsigned NOT NULL,
    `warning` varchar(2048) NOT NULL,
    PRIMARY KEY (`id`),
    KEY `proc_warnings__transaction` (`transaction_id`)
);

CREATE TABLE  `day_position` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `transaction_id` int(10) unsigned DEFAULT NULL,
    `sort_index` int(11) DEFAULT NULL,
    `ivehicle_day_id` int(10) unsigned DEFAULT NULL,
    `dirty_data` tinyint(4) DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `day_position__trans` (`transaction_id`),
    KEY `day_position__is` (`ivehicle_day_id`,`sort_index`),
    KEY `day_position__id` (`ivehicle_day_id`,`dirty_data`)
) ;

CREATE TABLE  `ivehicle_days` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `d` date DEFAULT NULL,
    `sort_index` int(11) DEFAULT NULL,
    `ivehicle_id` int(10) unsigned DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `ivehicle_days__is` (`ivehicle_id`,`sort_index`),
    KEY `ivehicle_days__d` (`d`)
);

Las consultas por transacción son las siguientes:

  • Transacción 1

    set transaction isolation level read committed;
    set autocommit=0;
    begin;
    DELETE pw 
    FROM proc_warnings pw 
    INNER JOIN day_position dp 
        ON dp.transaction_id = pw.transaction_id 
    INNER JOIN ivehicle_days vd 
        ON vd.id = dp.ivehicle_day_id 
    WHERE vd.ivehicle_id=2 AND dp.dirty_data=1;
  • Transacción 2

    set transaction isolation level read committed;
    set autocommit=0;
    begin;
    DELETE pw 
    FROM proc_warnings pw 
    INNER JOIN day_position dp 
        ON dp.transaction_id = pw.transaction_id 
    INNER JOIN ivehicle_days vd 
        ON vd.id = dp.ivehicle_day_id 
    WHERE vd.ivehicle_id=13 AND dp.dirty_data=1;

Uno de ellos siempre falla con el error "Tiempo de espera de bloqueo excedido ...". El information_schema.innodb_trxcontiene las siguientes filas:

| trx_id     | trx_state   | trx_started           | trx_requested_lock_id  | trx_wait_started      | trx_wait | trx_mysql_thread_id | trx_query |
| '1A2973A4' | 'LOCK WAIT' | '2012-12-12 20:03:25' | '1A2973A4:0:3172298:2' | '2012-12-12 20:03:25' | '2'      | '3089'              | 'DELETE pw FROM proc_warnings pw INNER JOIN day_position dp ON dp.transaction_id = pw.transaction_id INNER JOIN ivehicle_days vd ON vd.id = dp.ivehicle_day_id WHERE vd.ivehicle_id=13 AND dp.dirty_data=1' |
| '1A296F67' | 'RUNNING'   | '2012-12-12 19:58:02' | NULL                   | NULL | '7' | '3087' | NULL |

information_schema.innodb_locks

| lock_id                | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data |
| '1A2973A4:0:3172298:2' | '1A2973A4'  | 'X'       | 'RECORD'  | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |
| '1A296F67:0:3172298:2' | '1A296F67'  | 'X'       | 'RECORD'  | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |

Como puedo ver, ambas consultas quieren un Xbloqueo exclusivo en una fila con clave primaria = 53. Sin embargo, ninguna de ellas debe eliminar filas de la proc_warningstabla. Simplemente no entiendo por qué el índice está bloqueado. Además, el índice no se bloquea cuando la proc_warningstabla está vacía o la day_positiontabla contiene menos filas (es decir, cien filas).

La investigación adicional debía pasar por EXPLAINencima de la SELECTconsulta similar . Muestra que el optimizador de consultas no usa el índice para consultar la proc_warningstabla y esa es la única razón por la que puedo imaginar por qué bloquea todo el índice de la clave primaria.

Caso simplificado

El problema también se puede reproducir en un caso más simple cuando solo hay dos tablas con un par de registros, pero la tabla secundaria no tiene un índice en la columna de referencia de la tabla principal.

Crear parenttabla

CREATE TABLE `parent` (
  `id` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB

Crear childtabla

CREATE TABLE `child` (
  `id` int(10) unsigned NOT NULL,
  `parent_id` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB

Llenar tablas

INSERT INTO `parent` (id) VALUES (1), (2);
INSERT INTO `child` (id, parent_id) VALUES (1, NULL), (2, NULL);

Prueba en dos transacciones paralelas:

  • Transacción 1

    SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
    SET AUTOCOMMIT=0;
    BEGIN;
    DELETE c FROM child c 
      INNER JOIN parent p ON p.id = c.parent_id 
    WHERE p.id = 1;
  • Transacción 2

    SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
    SET AUTOCOMMIT=0;
    BEGIN;
    DELETE c FROM child c 
      INNER JOIN parent p ON p.id = c.parent_id 
    WHERE p.id = 2;

La parte común en ambos casos es que MySQL no usa índices. Creo que esa es la razón del bloqueo de toda la mesa.

Nuestra solución

La única solución que podemos ver por ahora es aumentar el tiempo de espera de bloqueo predeterminado de 50 segundos a 500 segundos para permitir que el hilo termine de limpiarse. Luego mantén los dedos cruzados.

Cualquier ayuda apreciada.

vitalidze
fuente
Tengo una pregunta: ¿Ejecutó COMMIT en alguna de las transacciones?
RolandoMySQLDBA
Por supuesto. El problema es que todas las demás transacciones deben esperar hasta que una de ellas confirme los cambios. El caso de prueba simple no contiene una declaración de confirmación para mostrar cómo reproducir el problema. Si ejecuta commit o rollback en una transacción sin espera, libera el bloqueo simultáneamente y la transacción en espera completa su trabajo.
vitalidze
Cuando dice que MySQL no usa índices en ninguno de los casos, ¿es porque no hay ninguno en el escenario real? Si hay índices, ¿podría proporcionarles el código? ¿Es posible probar alguna de las sugerencias de índice publicadas a continuación? Si no hay índices y no es posible intentar agregar ninguno, MySQL no puede restringir el conjunto de datos procesados ​​por cada subproceso. Si ese es el caso, entonces N subprocesos simplemente multiplicarían la carga de trabajo del servidor por N veces, y sería más eficiente dejar que un subproceso se ejecute con una lista de parámetros como {WHERE vd.ivehicle_id IN (2, 13) AND dp.dirty_data = 1;}.
JM Hicks
Ok, encontramos los índices escondidos en el archivo de datos de muestra vinculado.
JM Hicks
Un par de preguntas más: 1) ¿Cuántas filas day_positioncontiene normalmente la tabla, cuando comienza a funcionar tan lento que tiene que aumentar el límite de tiempo de espera a 500 segundos? 2) ¿Cuánto tiempo se tarda en ejecutar cuando solo tiene los datos de muestra?
JM Hicks

Respuestas:

3

NUEVA RESPUESTA (SQL dinámico de estilo MySQL): Ok, este aborda el problema en la forma en que se describe uno de los otros carteles, invirtiendo el orden en el que se adquieren bloqueos exclusivos mutuamente incompatibles para que, independientemente de cuántos ocurran, ocurran solo para la menor cantidad de tiempo al final de la ejecución de la transacción.

Esto se logra separando la parte leída de la declaración en su propia declaración de selección y generando dinámicamente una declaración de eliminación que se verá obligada a ejecutarse en último lugar debido al orden de aparición de la declaración, y que afectará solo a la tabla proc_warnings.

Una demostración está disponible en sql fiddle:

Este enlace muestra el esquema con datos de muestra y una consulta simple para las filas que coinciden ivehicle_id=2. Se obtienen 2 filas, ya que ninguna de ellas ha sido eliminada.

Este enlace muestra el mismo esquema, datos de muestra, pero pasa un valor 2 al programa almacenado DeleteEntries, diciéndole al SP que elimine las proc_warningsentradas ivehicle_id=2. La consulta simple para filas no devuelve ningún resultado, ya que todas se han eliminado con éxito. Los enlaces de demostración solo demuestran que el código funciona según lo previsto para eliminar. El usuario con el entorno de prueba adecuado puede comentar si esto resuelve el problema del subproceso bloqueado.

Aquí está el código también por conveniencia:

CREATE PROCEDURE DeleteEntries (input_vid INT)
BEGIN

    SELECT @idstring:= '';
    SELECT @idnum:= 0;
    SELECT @del_stmt:= '';

    SELECT @idnum:= @idnum+1 idnum_col, @idstring:= CONCAT(@idstring, CASE WHEN CHARACTER_LENGTH(@idstring) > 0 THEN ',' ELSE '' END, CAST(id AS CHAR(10))) idstring_col
    FROM proc_warnings
    WHERE EXISTS (
        SELECT 0
        FROM day_position
        WHERE day_position.transaction_id = proc_warnings.transaction_id
        AND day_position.dirty_data = 1
        AND EXISTS (
            SELECT 0
            FROM ivehicle_days
            WHERE ivehicle_days.id = day_position.ivehicle_day_id
            AND ivehicle_days.ivehicle_id = input_vid
        )
    )
    ORDER BY idnum_col DESC
    LIMIT 1;

    IF (@idnum > 0) THEN
        SELECT @del_stmt:= CONCAT('DELETE FROM proc_warnings WHERE id IN (', @idstring, ');');

        PREPARE del_stmt_hndl FROM @del_stmt;
        EXECUTE del_stmt_hndl;
        DEALLOCATE PREPARE del_stmt_hndl;
    END IF;
END;

Esta es la sintaxis para llamar al programa desde una transacción:

CALL DeleteEntries(2);

RESPUESTA ORIGINAL (todavía creo que no es demasiado lamentable) Parece 2 problemas: 1) consulta lenta 2) comportamiento de bloqueo inesperado

Con respecto al problema # 1, las consultas lentas a menudo se resuelven mediante las mismas dos técnicas en la simplificación de la declaración de consulta en tándem y adiciones útiles o modificaciones a los índices. Usted mismo ya realizó la conexión a los índices: sin ellos, el optimizador no puede buscar un conjunto limitado de filas para procesar, y cada fila de cada tabla que se multiplica por fila adicional escanea la cantidad de trabajo adicional que debe hacerse.

REVISADO DESPUÉS DE VER LA PUBLICACIÓN DE ESQUEMA E ÍNDICES: Pero imagino que obtendrá el mayor beneficio de rendimiento para su consulta asegurándose de tener una buena configuración de índice. Para hacerlo, puede obtener un mejor rendimiento de eliminación, y posiblemente incluso un mejor rendimiento de eliminación, con el intercambio de índices más grandes y quizás un rendimiento de inserción notablemente más lento en las mismas tablas a las que se agrega una estructura de índice adicional.

UN POCO MEJOR:

CREATE TABLE  `day_position` (
    ...,
    KEY `day_position__id_rvrsd` (`dirty_data`, `ivehicle_day_id`)

) ;


CREATE TABLE  `ivehicle_days` (
    ...,
    KEY `ivehicle_days__vid_no_sort_index` (`ivehicle_id`)
);

REVISADO AQUÍ TAMBIÉN: Dado que lleva tanto tiempo ejecutarlo, dejaría los datos sucios en el índice, y también me equivoqué cuando lo coloqué después del ivehicle_day_id en el orden del índice, debería ser el primero.

Pero si lo tuviera en mis manos, en este punto, ya que debe haber una buena cantidad de datos para que tome tanto tiempo, simplemente iría a todos los índices de cobertura solo para asegurarme de que obtuviera la mejor indexación que mi tiempo de solución de problemas podría comprar, si nada más para descartar esa parte del problema.

MEJORES / ÍNDICES DE CUBIERTA:

CREATE TABLE  `day_position` (
    ...,
    KEY `day_position__id_rvrsd_trnsid_cvrng` (`dirty_data`, `ivehicle_day_id`, `transaction_id`)
) ;

CREATE TABLE  `ivehicle_days` (
    ...,
    UNIQUE KEY `ivehicle_days__vid_id_cvrng` (ivehicle_id, id)
);

CREATE TABLE  `proc_warnings` (

    .., /*rename primary key*/
    CONSTRAINT pk_proc_warnings PRIMARY KEY (id),
    UNIQUE KEY `proc_warnings__transaction_id_id_cvrng` (`transaction_id`, `id`)
);

Hay dos objetivos de optimización del rendimiento buscados por las dos últimas sugerencias de cambio:
1) Si las claves de búsqueda para las tablas a las que se accede sucesivamente no son las mismas que los resultados clave agrupados devueltos para la tabla a la que se accede actualmente, eliminamos lo que habría sido necesario hacer un segundo conjunto de operaciones de búsqueda de índice con exploración en el índice agrupado
2) Si este último no es el caso, todavía existe al menos la posibilidad de que el optimizador pueda seleccionar un algoritmo de unión más eficiente ya que los índices mantendrán el requiere unir claves en orden ordenado.

Su consulta parece tan simplificada como puede ser (copiada aquí en caso de que se edite más adelante):

DELETE pw 
FROM proc_warnings pw 
INNER JOIN day_position dp 
    ON dp.transaction_id = pw.transaction_id 
INNER JOIN ivehicle_days vd 
    ON vd.id = dp.ivehicle_day_id 
WHERE vd.ivehicle_id=2 AND dp.dirty_data=1;

A menos que, por supuesto, haya algo sobre el orden de combinación escrito que afecte la forma en que el optimizador de consultas avanza, en cuyo caso podría probar algunas de las sugerencias de reescritura que otros han proporcionado, incluida quizás esta con sugerencias de índice (opcional):

DELETE FROM proc_warnings
FORCE INDEX (`proc_warnings__transaction_id_id_cvrng`, `pk_proc_warnings`)
WHERE EXISTS (
    SELECT 0
    FROM day_position
    FORCE INDEX (`day_position__id_rvrsd_trnsid_cvrng`)  
    WHERE day_position.transaction_id = proc_warnings.transaction_id
    AND day_position.dirty_data = 1
    AND EXISTS (
        SELECT 0
        FROM ivehicle_days
        FORCE INDEX (`ivehicle_days__vid_id_cvrng`)  
        WHERE ivehicle_days.id = day_position.ivehicle_day_id
        AND ivehicle_days.ivehicle_id = ?
    )
);

En cuanto al # 2, comportamiento de bloqueo inesperado.

Como puedo ver, ambas consultas quieren un bloqueo X exclusivo en una fila con clave primaria = 53. Sin embargo, ninguno de ellos debe eliminar filas de la tabla proc_warnings. Simplemente no entiendo por qué el índice está bloqueado.

Supongo que sería el índice el que está bloqueado porque la fila de datos a bloquear está en un índice agrupado, es decir, la fila de datos en sí reside en el índice.

Estaría bloqueado porque:
1) de acuerdo con http://dev.mysql.com/doc/refman/5.1/en/innodb-locks-set.html

... un DELETE generalmente establece bloqueos de registro en cada registro de índice que se escanea en el procesamiento de la instrucción SQL. No importa si hay condiciones WHERE en la declaración que excluirían la fila. InnoDB no recuerda la condición exacta de WHERE, pero solo sabe qué rangos de índice se analizaron.

También mencionaste anteriormente:

... en cuanto a mí, la característica principal de READ COMMITTED es cómo trata las cerraduras. Debería liberar los bloqueos de índice de las filas no coincidentes, pero no lo hace.

y proporcionó la siguiente referencia para eso:
http://dev.mysql.com/doc/refman/5.1/en/set-transaction.html#isolevel_read-committed

Que establece lo mismo que usted, excepto que de acuerdo con esa misma referencia hay una condición sobre la cual se liberará un bloqueo:

Además, los bloqueos de registro para filas no coincidentes se liberan después de que MySQL ha evaluado la condición WHERE.

Lo cual también se reitera en la página de este manual http://dev.mysql.com/doc/refman/5.1/en/innodb-record-level-locks.html

También hay otros efectos de usar el nivel de aislamiento READ COMMITTED o habilitar innodb_locks_unsafe_for_binlog: los bloqueos de registro para filas no coincidentes se liberan después de que MySQL ha evaluado la condición WHERE.

Por lo tanto, se nos dice que la condición WHERE debe evaluarse antes de que el bloqueo se pueda volver a lanzar. Desafortunadamente, no se nos dice cuándo se evalúa la condición WHERE y probablemente algo esté sujeto a cambios de un plan a otro creado por el optimizador. Pero sí nos dice que la liberación del bloqueo depende de alguna manera del rendimiento de la ejecución de la consulta, cuya optimización, como discutimos anteriormente, depende de la escritura cuidadosa de la declaración y el uso juicioso de los índices. También se puede mejorar con un mejor diseño de tabla, pero eso probablemente se dejaría mejor en una pregunta separada.

Además, el índice tampoco está bloqueado cuando la tabla proc_warnings está vacía

La base de datos no puede bloquear registros dentro del índice si no hay ninguno.

Además, el índice no está bloqueado cuando ... la tabla day_position contiene menos número de filas (es decir, cien filas).

Esto podría significar numerosas cosas como, entre otras: un plan de ejecución diferente debido a un cambio en las estadísticas, un bloqueo demasiado breve para ser observado debido a una ejecución mucho más rápida debido a un conjunto de datos mucho más pequeño / unirse a la operación.

JM Hicks
fuente
La WHEREcondición se evalúa cuando se completa la consulta. ¿No es así? Pensé que el bloqueo se libera justo después de que se ejecuten algunas consultas concurrentes. Ese es el comportamiento natural. Sin embargo, esto no sucede. Ninguna de las consultas sugeridas en este hilo ayuda a evitar el bloqueo de índice agrupado en la proc_warningstabla. Creo que presentaré un error en MySQL. Gracias por tu ayuda.
vitalidze
Tampoco esperaría que evitaran el comportamiento de bloqueo. Esperaría que se bloqueara porque creo que la documentación dice que eso es lo que se espera, así sea o no la forma en que queremos que procese la consulta. Solo esperaría que deshacerse del problema de rendimiento evitará que la consulta concurrente se bloquee durante un tiempo tan obviamente (500+ segundos de tiempo de espera).
JM Hicks
Aunque parece que {WHERE} podría usarse durante el procesamiento de la combinación para restringir qué filas se incluyen en el cálculo de la combinación, no veo cómo se podría evaluar su cláusula {WHERE} por fila bloqueada hasta que todo el conjunto de combinaciones esté calculado también. Dicho esto, para nuestro análisis, sospecho que tiene razón en que debemos sospechar "La condición WHERE se evalúa cuando se completa la consulta". Sin embargo, eso me lleva a la misma conclusión general, que el rendimiento debe resolverse, y luego el grado aparente de concurrencia aumentará proporcionalmente.
JM Hicks
Recuerde que los índices adecuados pueden eliminar cualquier exploración de tabla completa que ocurra en la tabla proc_warnings. Para que eso suceda, necesitamos que el optimizador de consultas funcione bien para nosotros, y necesitamos nuestros índices, consultas y datos para que funcionen bien con él. Los valores de los parámetros deben evaluarse al final de las filas de la tabla de destino que no se superponen entre las dos consultas. Los índices deben proporcionar al optimizador de consultas un medio para buscar eficientemente esas filas. Necesitamos el optimizador para darnos cuenta de esa potencial eficiencia de búsqueda y seleccionar dicho plan.
JM Hicks
Si todo va bien entre los valores de los parámetros, los índices, los resultados que no se superponen en la tabla proc_warnings y la selección del plan optimizador, incluso si se pueden generar bloqueos durante el tiempo necesario para ejecutar la consulta para cada hilo, esos bloqueos, si no superpuesto, no entrará en conflicto con las solicitudes de bloqueo de los otros hilos.
JM Hicks
3

Puedo ver cómo READ_COMMITTED puede causar esta situación.

READ_COMMITTED permite tres cosas:

  • Visibilidad de cambios confirmados por otras transacciones utilizando el nivel de aislamiento READ_COMMITTED .
  • Lecturas no repetibles: transacción que realiza la misma recuperación con la posibilidad de obtener un resultado diferente cada vez.
  • Fantasmas: las transacciones pueden tener filas que aparecen donde no era visible de antemano.

Esto crea un paradigma interno para la transacción en sí porque la transacción debe mantener contacto con:

  • InnoDB Buffer Pool (mientras que commit todavía está vacío)
  • Clave primaria de la tabla
  • Posiblemente
    • el búfer de doble escritura
    • Deshacer espacio de tabla
  • Representación pictórica

Si dos transacciones READ_COMMITTED distintas están accediendo a las mismas tablas / filas que se están actualizando de la misma manera, esté preparado para esperar no un bloqueo de tabla, sino un bloqueo exclusivo dentro del gen_clust_index (también conocido como Índice agrupado) . Dadas las consultas de su caso simplificado:

  • Transacción 1

    SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
    SET AUTOCOMMIT=0;
    BEGIN;
    DELETE c FROM child c 
      INNER JOIN parent p ON p.id = c.parent_id 
    WHERE p.id = 1;
  • Transacción 2

    SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
    SET AUTOCOMMIT=0;
    BEGIN;
    DELETE c FROM child c 
      INNER JOIN parent p ON p.id = c.parent_id 
    WHERE p.id = 2;

Está bloqueando la misma ubicación en gen_clust_index. Se puede decir, "pero cada transacción tiene una clave primaria diferente". Desafortunadamente, este no es el caso a los ojos de InnoDB. Sucede que la identificación 1 y la identificación 2 residen en la misma página.

Mire hacia atrás en el information_schema.innodb_lockssuministro de la pregunta

| lock_id                | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data |
| '1A2973A4:0:3172298:2' | '1A2973A4'  | 'X'       | 'RECORD'  | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |
| '1A296F67:0:3172298:2' | '1A296F67'  | 'X'       | 'RECORD'  | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |

Con la excepción de lock_id, lock_trx_idel resto de la descripción de la cerradura es idéntica. Dado que las transacciones están en el mismo campo de juego (mismo aislamiento de transacción), esto debería suceder .

Créeme, he abordado este tipo de situación antes. Aquí están mis publicaciones anteriores sobre esto:

RolandoMySQLDBA
fuente
He leído sobre las cosas que describe en los documentos de MySQL. Pero en cuanto a mí, la característica principal de READ COMMITTED es cómo se ocupa de las cerraduras . Debería liberar los bloqueos de índice de las filas no coincidentes, pero no lo hace.
vitalidze
Si solo una declaración SQL se revierte como resultado de un error, algunos de los bloqueos establecidos por la declaración pueden conservarse. Esto sucede porque guarda los bloqueos de InnoDB fila en un formato tal que no pueden saber lo que después de bloqueo se establece por qué afirmación: dev.mysql.com/doc/refman/5.5/en/innodb-deadlock-detection.html
RolandoMySQLDBA
Tenga en cuenta que mencioné la posibilidad de dos filas existentes en la misma página para el bloqueo (Ver Look back at information_schema.innodb_locks you supplied in the Question)
RolandoMySQLDBA
Acerca de la reversión de una sola declaración: entiendo esto como si una sola declaración falla dentro de una sola transacción, aún puede retener los bloqueos. Está bien. Mi gran pregunta es por qué no libera bloqueos de fila no coincidentes después de procesar correctamente la DELETEdeclaración.
vitalidze
Con dos bloqueos completos, uno tiene que revertirse. Es posible que las cerraduras permanezcan. TEORÍA DE TRABAJO: la transacción que retrocedió puede volver a intentarlo y puede encontrar un bloqueo antiguo de la transacción anterior que lo retuvo.
RolandoMySQLDBA
2

Miré la consulta y la explicación. No estoy seguro, pero tengo el presentimiento de que el problema es el siguiente. Veamos la consulta:

DELETE pw 
FROM proc_warnings pw 
INNER JOIN day_position dp 
   ON dp.transaction_id = pw.transaction_id 
INNER JOIN ivehicle_days vd 
   ON vd.id = dp.ivehicle_day_id 
WHERE vd.ivehicle_id=? AND dp.dirty_data=1;

El SELECT equivalente es:

SELECT pw.id
FROM proc_warnings pw
INNER JOIN day_position dp
   ON dp.transaction_id = pw.transaction_id
INNER JOIN ivehicle_days vd
   ON vd.id = dp.ivehicle_day_id
WHERE vd.ivehicle_id=16 AND dp.dirty_data=1;

Si observa su explicación, verá que el plan de ejecución comienza con la proc_warningstabla. Eso significa que MySQL escanea la clave primaria en la tabla y, para cada fila, verifica si la condición es verdadera y, si lo es, la fila se elimina. Es decir, MySQL tiene que bloquear toda la clave primaria.

Lo que necesita es invertir el orden JOIN, es decir, encontrar todos los identificadores de transacción vd.ivehicle_id=16 AND dp.dirty_data=1y unirlos en la proc_warningstabla.

Es decir, deberá parchear uno de los índices:

ALTER TABLE `day_position`
 DROP INDEX `day_position__id`,
 ADD INDEX `day_position__id`
   USING BTREE (`ivehicle_day_id`, `dirty_data`, `transaction_id`);

y reescribe la consulta de eliminación:

DELETE pw
FROM (
  SELECT DISTINCT dp.transaction_id
  FROM ivehicle_days vd
  JOIN day_position dp ON dp.ivehicle_day_id = vd.id
  WHERE vd.ivehicle_id=? AND dp.dirty_data=1
) as tr_id
JOIN proc_warnings pw ON pw.transaction_id = tr_id.transaction_id;
nuevo
fuente
Desafortunadamente, esto no ayuda, es decir, las filas proc_warningsaún se bloquean. Gracias de cualquier manera.
vitalidze
2

Cuando configura el nivel de transacción sin la forma en que lo hace, aplica la lectura confirmada solo a la siguiente transacción, por lo tanto (configure la confirmación automática). Esto significa que después de autocommitir = 0, ya no estás en lectura confirmada. Lo escribiría de esta manera:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
DELETE c FROM child c
INNER JOIN parent p ON
    p.id = c.parent_id
WHERE p.id = 1;

Puede verificar en qué nivel de aislamiento se encuentra consultando

SELECT @@tx_isolation;
Franck
fuente
Eso no es cierto. ¿Por qué SET AUTOCOMMIT=0debería restablecer el nivel de aislamiento para la próxima transacción? Creo que comienza una nueva transacción si ninguna se inició antes (que es mi caso). Por lo tanto, para ser más precisos, la siguiente START TRANSACTIONo BEGINdeclaración no es necesaria. Mi propósito de deshabilitar la confirmación automática es dejar la transacción abierta después de la DELETEejecución de la declaración.
vitalidze
1
@SqlKiwi esta fue la forma de editar esta publicación, y esta fue la única para comentar ;-)
jcolebrand