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?Réponses:
NOUVELLE RÉPONSE (SQL dynamique de style MySQL): OK, celui-ci s'attaque au problème de la manière décrite par l'une des autres affiches - inversant l'ordre dans lequel les verrous exclusifs mutuellement incompatibles sont acquis de sorte que, quel que soit le nombre, ils se produisent uniquement pour le moins de temps à la fin de l'exécution de la transaction.
Ceci est accompli en séparant la partie lue de l'instruction en sa propre instruction select et en générant dynamiquement une instruction delete qui sera forcée de s'exécuter en dernier en raison de l'ordre d'apparition de l'instruction, et qui n'affectera que la table proc_warnings.
Une démo est disponible chez sql fiddle:
Ce lien affiche le schéma avec des exemples de données et une simple requête pour les lignes qui correspondent
ivehicle_id=2
. Résultat: 2 lignes car aucune n'a été supprimée.Ce lien affiche le même schéma, des exemples de données, mais transmet une valeur 2 au programme stocké DeleteEntries, indiquant au SP de supprimer les
proc_warnings
entrées pourivehicle_id=2
. La requête simple pour les lignes ne renvoie aucun résultat car elles ont toutes été supprimées avec succès. Les liens de démonstration montrent uniquement que le code fonctionne comme prévu pour le supprimer. L'utilisateur disposant de l'environnement de test approprié peut indiquer si cela résout le problème du thread bloqué.Voici également le code pour plus de commodité:
Voici la syntaxe pour appeler le programme à partir d'une transaction:
RÉPONSE ORIGINALE (pense toujours que ce n'est pas trop minable) Ressemble à 2 problèmes: 1) requête lente 2) comportement de verrouillage inattendu
En ce qui concerne le problème n ° 1, les requêtes lentes sont souvent résolues par les deux mêmes techniques dans la simplification des instructions de requête en tandem et les ajouts ou modifications utiles aux index. Vous avez vous-même déjà établi la connexion aux index - sans eux, l'optimiseur ne peut pas rechercher un ensemble limité de lignes à traiter, et chaque ligne de chaque table multipliant par ligne supplémentaire a analysé la quantité de travail supplémentaire qui doit être effectuée.
RÉVISÉ APRÈS VOIR LE POSTE DE SCHÉMA ET D'INDEX: Mais j'imagine que vous obtiendrez le plus d'avantages en termes de performances pour votre requête en vous assurant que vous avez une bonne configuration d'index. Pour ce faire, vous pouvez opter pour de meilleures performances de suppression, et peut-être même de meilleures performances de suppression, avec un compromis d'index plus grands et peut-être des performances d'insertion sensiblement plus lentes sur les mêmes tables auxquelles une structure d'index supplémentaire est ajoutée.
QUELQUE PEU MEILLEUR:
RÉVISÉ ICI AUSSI: Étant donné qu'il faut autant de temps pour s'exécuter, je laisserais les dirty_data dans l'index, et je me suis trompé aussi à coup sûr quand je l'ai placé après ivehicle_day_id dans l'ordre de l'index - il devrait être le premier.
Mais si je mettais la main dessus, à ce stade, car il doit y avoir une bonne quantité de données pour que cela prenne autant de temps, je choisirais tous les index de couverture juste pour m'assurer d'obtenir la meilleure indexation qui soit. mon temps de dépannage pourrait acheter, si rien d'autre pour exclure cette partie du problème.
MEILLEURS INDICES DE COUVERTURE:
Les deux dernières suggestions de modification visent deux objectifs d'optimisation des performances:
1) Si les clés de recherche des tables accédées successivement ne sont pas les mêmes que les résultats des clés en cluster renvoyées pour la table actuellement accédée, nous éliminons ce qui aurait dû être fait un deuxième ensemble d'opérations de recherche d'index avec analyse sur l'index clusterisé
2) Si ce dernier n'est pas le cas, il y a toujours au moins la possibilité que l'optimiseur puisse sélectionner un algorithme de jointure plus efficace puisque les index conserveront le Clés de jointure requises dans l'ordre trié.
Votre requête semble aussi simplifiée que possible (copiée ici au cas où elle serait modifiée ultérieurement):
À moins bien sûr qu'il y ait quelque chose dans l'ordre de jointure écrit qui affecte la façon dont l'optimiseur de requête procède, auquel cas vous pouvez essayer certaines des suggestions de réécriture que d'autres ont fournies, y compris peut-être celle-ci avec des indices (facultatif):
En ce qui concerne # 2, comportement de verrouillage inattendu.
Je suppose que ce serait l'index qui est verrouillé parce que la ligne de données à verrouiller se trouve dans un index clusterisé, c'est-à-dire que la seule ligne de données elle-même réside dans l'index.
Il serait verrouillé, car:
1) selon http://dev.mysql.com/doc/refman/5.1/en/innodb-locks-set.html
Vous avez également mentionné ci-dessus:
et a fourni la référence suivante pour cela:
http://dev.mysql.com/doc/refman/5.1/en/set-transaction.html#isolevel_read-committed
Qui dit la même chose que vous, sauf que selon cette même référence, il y a une condition à laquelle un verrou doit être libéré:
Ce qui est également réitéré sur cette page de manuel http://dev.mysql.com/doc/refman/5.1/en/innodb-record-level-locks.html
Ainsi, on nous dit que la condition WHERE doit être évaluée avant que le verrou puisse être relâché. Malheureusement, on ne nous dit pas quand la condition WHERE est évaluée et cela pourrait probablement changer quelque chose d'un plan à un autre créé par l'optimiseur. Mais cela nous dit que la libération du verrou dépend en quelque sorte des performances de l'exécution des requêtes, dont l'optimisation, comme nous le discutons ci-dessus, dépend de l'écriture minutieuse de l'instruction et de l'utilisation judicieuse des index. Il peut également être amélioré par une meilleure conception de la table, mais il serait probablement préférable de laisser une question distincte.
La base de données ne peut pas verrouiller les enregistrements dans l'index s'il n'y en a pas.
Cela pourrait signifier de nombreuses choses telles que mais sans s'y limiter: un plan d'exécution différent en raison d'un changement de statistiques, un verrou trop bref pour être observé en raison d'une exécution beaucoup plus rapide en raison d'un ensemble de données beaucoup plus petit / rejoindre l'opération.
la source
WHERE
condition est évaluée à la fin de la requête. N'est-ce pas? Je pensais que le verrou était libéré juste après l'exécution de certaines requêtes simultanées. Voilà le comportement naturel. Cependant, cela ne se produit pas. Aucune des requêtes suggérées dans ce thread ne permet d'éviter le verrouillage d'index cluster dans laproc_warnings
table. Je pense que je vais déposer un bug sur MySQL. Merci de votre aide.Je peux voir comment READ_COMMITTED peut provoquer cette situation.
READ_COMMITTED permet trois choses:
Cela crée un paradigme interne pour la transaction elle-même car la transaction doit maintenir le contact avec:
Si deux transactions READ_COMMITTED distinctes accèdent aux mêmes tables / lignes qui sont mises à jour de la même manière, soyez prêt à vous attendre non pas à un verrou de table, mais à un verrou exclusif dans gen_clust_index (aka Clustered Index) . Compte tenu des requêtes de votre cas simplifié:
Transaction 1
Transaction 2
Vous verrouillez le même emplacement dans gen_clust_index. On peut dire "mais chaque transaction a une clé primaire différente". Malheureusement, ce n'est pas le cas aux yeux d'InnoDB. Il se trouve que les identifiants 1 et 2 résident sur la même page.
Revenez sur
information_schema.innodb_locks
vous fourni dans la questionÀ l'exception de
lock_id
,lock_trx_id
le reste de la description du verrou est identique. Étant donné que les transactions sont sur un pied d'égalité (même isolement des transactions), cela devrait en effet se produire .Croyez-moi, j'ai déjà abordé ce genre de situation. Voici mes précédents articles à ce sujet:
Nov 05, 2012
: Comment analyser le statut innodb sur blocage dans l'insertion de requête?Aug 08, 2011
: Les blocages InnoDB sont - ils exclusifs à INSERT / UPDATE / DELETE?Jun 14, 2011
: Raisons des requêtes parfois lentes?Jun 08, 2011
: Ces deux requêtes entraîneront-elles un blocage si elles sont exécutées en séquence?Jun 06, 2011
: Difficulté à déchiffrer un blocage dans un journal d'état innodbla source
Look back at information_schema.innodb_locks you supplied in the Question
)DELETE
instruction.J'ai regardé la requête et l'expliquer. Je ne suis pas sûr, mais j'ai l'intuition, que le problème est le suivant. Regardons la requête:
Le SELECT équivalent est:
Si vous regardez son explication, vous verrez que le plan d'exécution commence par le
proc_warnings
tableau. Cela signifie que MySQL analyse la clé primaire de la table et vérifie pour chaque ligne si la condition est vraie, et si elle l'est - la ligne est supprimée. C'est-à-dire que MySQL doit verrouiller toute la clé primaire.Ce dont vous avez besoin est d'inverser l'ordre JOIN, c'est-à-dire de trouver tous les ID de transaction avec
vd.ivehicle_id=16 AND dp.dirty_data=1
et de les joindre sur laproc_warnings
table.C'est-à-dire que vous devrez patcher l'un des indices:
et réécrivez la requête de suppression:
la source
proc_warnings
toujours verrouillées. Merci quand même.Lorsque vous définissez le niveau de transaction sans la manière dont vous le faites, il applique la lecture validée à la transaction suivante uniquement, donc (définir la validation automatique). Cela signifie qu'après autocommit = 0, vous n'êtes plus en lecture validée. Je l'écrirais de cette façon:
Vous pouvez vérifier le niveau d'isolement dans lequel vous vous trouvez en interrogeant
la source
SET AUTOCOMMIT=0
devrait réinitialiser le niveau d'isolement pour la prochaine transaction? Je crois qu'il démarre une nouvelle transaction si aucune n'a été lancée auparavant (ce qui est mon cas). Donc, pour être plus précis, la déclaration suivanteSTART TRANSACTION
ouBEGIN
n'est pas nécessaire. Mon but de désactiver l'autocommit est de laisser la transaction ouverte après l'DELETE
exécution de l' instruction.