Prefacio
Nuestra aplicación ejecuta varios hilos que ejecutan DELETE
consultas en paralelo. Las consultas afectan a los datos aislados, es decir, no debería existir la posibilidad de que concurran DELETE
en las mismas filas de subprocesos separados. Sin embargo, según la documentación, MySQL usa el llamado bloqueo de 'siguiente clave' para las DELETE
declaraciones, 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 COMMITTED
nivel de aislamiento.
El problema
El problema surge cuando se ejecutan DELETE
declaraciones complejas con JOIN
s 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 JOIN
tablas 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 COMMITTED
modo 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_trx
contiene 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 X
bloqueo exclusivo en una fila con clave primaria = 53. Sin embargo, ninguna de ellas debe eliminar filas de la proc_warnings
tabla. Simplemente no entiendo por qué el índice está bloqueado. Además, el índice no se bloquea cuando la proc_warnings
tabla está vacía o la day_position
tabla contiene menos filas (es decir, cien filas).
La investigación adicional debía pasar por EXPLAIN
encima de la SELECT
consulta similar . Muestra que el optimizador de consultas no usa el índice para consultar la proc_warnings
tabla 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 parent
tabla
CREATE TABLE `parent` (
`id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
Crear child
tabla
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.
day_position
contiene 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?Respuestas:
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_warnings
entradasivehicle_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:
Esta es la sintaxis para llamar al programa desde una transacción:
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:
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:
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):
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):
En cuanto al # 2, comportamiento de bloqueo inesperado.
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
También mencionaste anteriormente:
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:
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
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.
La base de datos no puede bloquear registros dentro del índice si no hay ninguno.
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.
fuente
WHERE
condició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 laproc_warnings
tabla. Creo que presentaré un error en MySQL. Gracias por tu ayuda.Puedo ver cómo READ_COMMITTED puede causar esta situación.
READ_COMMITTED permite tres cosas:
Esto crea un paradigma interno para la transacción en sí porque la transacción debe mantener contacto con:
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
Transacción 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_locks
suministro de la preguntaCon la excepción de
lock_id
,lock_trx_id
el 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:
Nov 05, 2012
: ¿Cómo analizar el estado de innodb en punto muerto en la consulta de inserción?Aug 08, 2011
: ¿Los bloqueos muertos de InnoDB son exclusivos de INSERT / UPDATE / DELETE?Jun 14, 2011
: ¿ Razones para consultas lentas ocasionalmente?Jun 08, 2011
: ¿Estas dos consultas resultarán en un punto muerto si se ejecutan en secuencia?Jun 06, 2011
: Problemas para descifrar un punto muerto en un registro de estado innodbfuente
Look back at information_schema.innodb_locks you supplied in the Question
)DELETE
declaración.Miré la consulta y la explicación. No estoy seguro, pero tengo el presentimiento de que el problema es el siguiente. Veamos la consulta:
El SELECT equivalente es:
Si observa su explicación, verá que el plan de ejecución comienza con la
proc_warnings
tabla. 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=1
y unirlos en laproc_warnings
tabla.Es decir, deberá parchear uno de los índices:
y reescribe la consulta de eliminación:
fuente
proc_warnings
aún se bloquean. Gracias de cualquier manera.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:
Puede verificar en qué nivel de aislamiento se encuentra consultando
fuente
SET AUTOCOMMIT=0
deberí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 siguienteSTART TRANSACTION
oBEGIN
declaración no es necesaria. Mi propósito de deshabilitar la confirmación automática es dejar la transacción abierta después de laDELETE
ejecución de la declaración.