Prefazione
La nostra applicazione esegue diversi thread che eseguono DELETEquery in parallelo. Le query influiscono su dati isolati, ovvero non dovrebbe esserci alcuna possibilità che si DELETEverifichino contemporaneamente sulle stesse righe da thread separati. Tuttavia, per documentazione, MySQL utilizza il cosiddetto blocco "chiave successiva" per le DELETEistruzioni, 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 COMMITTEDlivello di isolamento.
Il problema
Il problema sorge quando si eseguono DELETEistruzioni complesse con JOINs 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 JOINtabelle 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 COMMITTEDmodalità 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_trxcontiene 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 Xblocco esclusivo su una riga con chiave primaria = 53. Tuttavia, nessuna delle due deve eliminare le righe dalla proc_warningstabella. Non capisco perché l'indice sia bloccato. Inoltre, l'indice non è bloccato né quando la proc_warningstabella è vuota o la day_positiontabella contiene un numero minore di righe (ovvero cento righe).
Ulteriore indagine fu di esaminare EXPLAINla SELECTquery simile . Mostra che Query Optimizer non utilizza l'indice per eseguire query sulla proc_warningstabella 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 parenttabella
CREATE TABLE `parent` (
`id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
Crea childtabella
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_positioncontiene 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?