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?