Prefazione
La nostra applicazione esegue diversi thread che eseguono DELETE
query in parallelo. Le query influiscono su dati isolati, ovvero non dovrebbe esserci alcuna possibilità che si DELETE
verifichino contemporaneamente sulle stesse righe da thread separati. Tuttavia, per documentazione, MySQL utilizza il cosiddetto blocco "chiave successiva" per le DELETE
istruzioni, che blocca sia la chiave corrispondente sia un po 'di spazio. Questa cosa porta a dead-lock e l'unica soluzione che abbiamo trovato è usare il READ COMMITTED
livello di isolamento.
Il problema
Il problema sorge quando si eseguono DELETE
istruzioni complesse con JOIN
s di tabelle enormi. In un caso particolare abbiamo una tabella con avvisi che ha solo due righe, ma la query deve eliminare tutti gli avvisi che appartengono ad alcune entità particolari da due INNER JOIN
tabelle ed separate . La query è la seguente:
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
Quando la tabella day_position è abbastanza grande (nel mio caso di test ci sono 1448 righe) qualsiasi transazione anche con la READ COMMITTED
modalità di isolamento blocca l' intera proc_warnings
tabella.
Il problema viene sempre riprodotto su questi dati di esempio: http://yadi.sk/d/QDuwBtpW1BxB9 sia in MySQL 5.1 (verificato su 5.1.59) che MySQL 5.5 (controllato su MySQL 5.5.24).
EDIT: i dati di esempio collegati contengono anche schemi e indici per le tabelle di query, riprodotti qui per comodità:
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`)
);
Le query per transazioni sono le seguenti:
Transazione 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;
Transazione 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 di questi fallisce sempre con l'errore "Blocca il timeout di attesa superato ...". Il information_schema.innodb_trx
contiene le seguenti righe:
| 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' |
Come posso vedere, entrambe le query richiedono un X
blocco esclusivo su una riga con chiave primaria = 53. Tuttavia, nessuna delle due deve eliminare le righe dalla proc_warnings
tabella. Non capisco perché l'indice sia bloccato. Inoltre, l'indice non è bloccato né quando la proc_warnings
tabella è vuota o la day_position
tabella contiene un numero minore di righe (ovvero cento righe).
Ulteriore indagine fu di esaminare EXPLAIN
la SELECT
query simile . Mostra che Query Optimizer non utilizza l'indice per eseguire query sulla proc_warnings
tabella e questa è l'unica ragione per cui posso immaginare perché blocca l'intero indice della chiave primaria.
Caso semplificato
Il problema può anche essere riprodotto in un caso più semplice quando ci sono solo due tabelle con un paio di record, ma la tabella figlio non ha un indice sulla colonna ref della tabella padre.
Crea parent
tabella
CREATE TABLE `parent` (
`id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
Crea child
tabella
CREATE TABLE `child` (
`id` int(10) unsigned NOT NULL,
`parent_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
Riempi le tabelle
INSERT INTO `parent` (id) VALUES (1), (2);
INSERT INTO `child` (id, parent_id) VALUES (1, NULL), (2, NULL);
Test in due transazioni parallele:
Transazione 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;
Transazione 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 comune in entrambi i casi è che MySQL non utilizza gli indici. Credo che sia la ragione del blocco dell'intero tavolo.
La nostra soluzione
L'unica soluzione che possiamo vedere per ora è aumentare il timeout di attesa del blocco predefinito da 50 secondi a 500 secondi per consentire la pulizia del filo. Quindi incrocia le dita.
Qualsiasi aiuto apprezzato.
day_position
contiene normalmente la tabella, quando inizia a funzionare così lentamente che devi superare il limite di timeout a 500 sec? 2) Quanto tempo ci vuole per avere solo i dati di esempio?