Préface
Notre application exécute plusieurs threads qui exécutent des DELETE
requêtes en parallèle. Les requêtes affectent des données isolées, c'est-à-dire qu'il ne devrait pas y avoir de possibilité de simultanéité DELETE
sur les mêmes lignes à partir de threads séparés. Cependant, selon la documentation, MySQL utilise ce que l'on appelle le verrouillage 'next-key' pour les DELETE
instructions, qui verrouille à la fois la clé correspondante et un certain écart. Cette chose mène à des blocages et la seule solution que nous avons trouvée est d'utiliser le READ COMMITTED
niveau d'isolement.
Le problème
Un problème survient lors de l'exécution d' DELETE
instructions complexes avec des JOIN
s d'énormes tables. Dans un cas particulier, nous avons une table avec des avertissements qui n'a que deux lignes, mais la requête doit supprimer tous les avertissements qui appartiennent à certaines entités particulières de deux INNER JOIN
tables ed distinctes . La requête est la suivante:
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
Lorsque la table day_position est suffisamment grande (dans mon cas de test, il y a 1448 lignes), toute transaction, même en READ COMMITTED
mode d'isolement, bloque la proc_warnings
table entière .
Le problème est toujours reproduit sur ces exemples de données - http://yadi.sk/d/QDuwBtpW1BxB9 à la fois dans MySQL 5.1 (vérifié sur 5.1.59) et MySQL 5.5 (vérifié sur MySQL 5.5.24).
EDIT: les exemples de données liées contiennent également un schéma et des index pour les tables de requête, reproduits ici pour plus de commodité:
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`)
);
Les requêtes par transaction sont les suivantes:
Transaction 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;
Transaction 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;
L'un d'eux échoue toujours avec l'erreur «Délai d'attente de verrouillage dépassé ...». Le information_schema.innodb_trx
contient les lignes suivantes:
| 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' |
Comme je peux le voir, les deux requêtes veulent un X
verrou exclusif sur une ligne avec la clé primaire = 53. Cependant, aucune d'entre elles ne doit supprimer des lignes de la proc_warnings
table. Je ne comprends tout simplement pas pourquoi l'index est verrouillé. De plus, l'index n'est pas verrouillé lorsque la proc_warnings
table est vide ou que la day_position
table contient moins de lignes (soit cent lignes).
Une enquête plus approfondie devait parcourir EXPLAIN
la SELECT
requête similaire . Il montre que l'optimiseur de requêtes n'utilise pas l'index pour interroger la proc_warnings
table et c'est la seule raison pour laquelle je peux imaginer pourquoi il bloque l'intégralité de l'index de clé primaire.
Cas simplifié
Le problème peut également être reproduit dans un cas plus simple lorsqu'il n'y a que deux tables avec quelques enregistrements, mais la table enfant n'a pas d'index sur la colonne de référence de la table parent.
Créer une parent
table
CREATE TABLE `parent` (
`id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
Créer une child
table
CREATE TABLE `child` (
`id` int(10) unsigned NOT NULL,
`parent_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
Remplissez les tableaux
INSERT INTO `parent` (id) VALUES (1), (2);
INSERT INTO `child` (id, parent_id) VALUES (1, NULL), (2, NULL);
Testez en deux transactions parallèles:
Transaction 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;
Transaction 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 partie commune dans les deux cas est que MySQL n'utilise pas d'index. Je crois que c'est la raison du verrouillage de toute la table.
Notre solution
La seule solution que nous pouvons voir pour l'instant est d'augmenter le délai d'attente de verrouillage par défaut de 50 secondes à 500 secondes pour permettre au fil de terminer le nettoyage. Gardez ensuite les doigts croisés.
Toute aide appréciée.
day_position
table contient-elle normalement, lorsqu'elle commence à s'exécuter si lentement que vous devez faire passer le délai d'expiration à 500 s? 2) Combien de temps faut-il pour s'exécuter lorsque vous ne disposez que des données d'exemple?