MySQL InnoDB verrouille la clé primaire lors de la suppression, même en LECTURE COMMISE

11

Préface

Notre application exécute plusieurs threads qui exécutent des DELETErequê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é DELETEsur 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 DELETEinstructions, 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 COMMITTEDniveau d'isolement.

Le problème

Un problème survient lors de l'exécution d' DELETEinstructions complexes avec des JOINs 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 JOINtables 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 COMMITTEDmode 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_trxcontient 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 Xverrou exclusif sur une ligne avec la clé primaire = 53. Cependant, aucune d'entre elles ne doit supprimer des lignes de la proc_warningstable. Je ne comprends tout simplement pas pourquoi l'index est verrouillé. De plus, l'index n'est pas verrouillé lorsque la proc_warningstable est vide ou que la day_positiontable contient moins de lignes (soit cent lignes).

Une enquête plus approfondie devait parcourir EXPLAINla SELECTrequête similaire . Il montre que l'optimiseur de requêtes n'utilise pas l'index pour interroger la proc_warningstable 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 parenttable

CREATE TABLE `parent` (
  `id` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB

Créer une childtable

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.

vitalidze
la source
J'ai une question: avez-vous exécuté COMMIT dans l'une des transactions?
RolandoMySQLDBA
Bien sûr. Le problème est que toutes les autres transactions doivent attendre jusqu'à ce que l'une d'entre elles valide les modifications. Le cas de test simple ne contient pas d'instruction de validation pour montrer comment reproduire le problème. Si vous exécutez la validation ou la restauration dans une transaction non en attente, il libère le verrou simultanément et la transaction en attente termine son travail.
vitalidze
Quand vous dites que MySQL n'utilise pas d'indices dans les deux cas, est-ce parce qu'il n'y en a pas dans le scénario réel? S'il existe des index, pourriez-vous leur fournir le code? Est-il possible d'essayer l'une des suggestions d'index ci-dessous? S'il n'y a pas d'index et qu'il n'est pas possible d'en ajouter, MySQL ne peut pas restreindre l'ensemble de données traité par chaque thread. Si tel est le cas, alors N threads multiplieraient simplement la charge de travail du serveur par N fois, et il serait plus efficace de simplement laisser un thread s'exécuter avec une liste de paramètres comme {WHERE vd.ivehicle_id IN (2, 13) AND dp.dirty_data = 1;}.
JM Hicks
Ok, j'ai trouvé les index cachés dans l'exemple de fichier de données lié.
JM Hicks
Quelques questions supplémentaires: 1) combien de lignes la day_positiontable 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?
JM Hicks

Réponses:

3

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_warningsentrées pour ivehicle_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é:

CREATE PROCEDURE DeleteEntries (input_vid INT)
BEGIN

    SELECT @idstring:= '';
    SELECT @idnum:= 0;
    SELECT @del_stmt:= '';

    SELECT @idnum:= @idnum+1 idnum_col, @idstring:= CONCAT(@idstring, CASE WHEN CHARACTER_LENGTH(@idstring) > 0 THEN ',' ELSE '' END, CAST(id AS CHAR(10))) idstring_col
    FROM proc_warnings
    WHERE EXISTS (
        SELECT 0
        FROM day_position
        WHERE day_position.transaction_id = proc_warnings.transaction_id
        AND day_position.dirty_data = 1
        AND EXISTS (
            SELECT 0
            FROM ivehicle_days
            WHERE ivehicle_days.id = day_position.ivehicle_day_id
            AND ivehicle_days.ivehicle_id = input_vid
        )
    )
    ORDER BY idnum_col DESC
    LIMIT 1;

    IF (@idnum > 0) THEN
        SELECT @del_stmt:= CONCAT('DELETE FROM proc_warnings WHERE id IN (', @idstring, ');');

        PREPARE del_stmt_hndl FROM @del_stmt;
        EXECUTE del_stmt_hndl;
        DEALLOCATE PREPARE del_stmt_hndl;
    END IF;
END;

Voici la syntaxe pour appeler le programme à partir d'une transaction:

CALL DeleteEntries(2);

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:

CREATE TABLE  `day_position` (
    ...,
    KEY `day_position__id_rvrsd` (`dirty_data`, `ivehicle_day_id`)

) ;


CREATE TABLE  `ivehicle_days` (
    ...,
    KEY `ivehicle_days__vid_no_sort_index` (`ivehicle_id`)
);

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:

CREATE TABLE  `day_position` (
    ...,
    KEY `day_position__id_rvrsd_trnsid_cvrng` (`dirty_data`, `ivehicle_day_id`, `transaction_id`)
) ;

CREATE TABLE  `ivehicle_days` (
    ...,
    UNIQUE KEY `ivehicle_days__vid_id_cvrng` (ivehicle_id, id)
);

CREATE TABLE  `proc_warnings` (

    .., /*rename primary key*/
    CONSTRAINT pk_proc_warnings PRIMARY KEY (id),
    UNIQUE KEY `proc_warnings__transaction_id_id_cvrng` (`transaction_id`, `id`)
);

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):

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;

À 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):

DELETE FROM proc_warnings
FORCE INDEX (`proc_warnings__transaction_id_id_cvrng`, `pk_proc_warnings`)
WHERE EXISTS (
    SELECT 0
    FROM day_position
    FORCE INDEX (`day_position__id_rvrsd_trnsid_cvrng`)  
    WHERE day_position.transaction_id = proc_warnings.transaction_id
    AND day_position.dirty_data = 1
    AND EXISTS (
        SELECT 0
        FROM ivehicle_days
        FORCE INDEX (`ivehicle_days__vid_id_cvrng`)  
        WHERE ivehicle_days.id = day_position.ivehicle_day_id
        AND ivehicle_days.ivehicle_id = ?
    )
);

En ce qui concerne # 2, comportement de verrouillage inattendu.

Comme je peux le voir, les deux requêtes veulent un verrou X exclusif sur une ligne avec la clé primaire = 53. Cependant, aucun d'eux ne doit supprimer des lignes de la table proc_warnings. Je ne comprends tout simplement pas pourquoi l'index est verrouillé.

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

... une suppression supprime généralement des verrous d'enregistrement sur chaque enregistrement d'index analysé lors du traitement de l'instruction SQL. Peu importe s'il existe des conditions WHERE dans l'instruction qui excluraient la ligne. InnoDB ne se souvient pas de la condition WHERE exacte, mais sait uniquement quelles plages d'index ont été analysées.

Vous avez également mentionné ci-dessus:

... quant à moi, la principale caractéristique de READ COMMITTED est la façon dont il traite les verrous. Il devrait libérer les verrous d'index des lignes non correspondantes, mais ce n'est pas le cas.

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é:

En outre, les verrous d'enregistrement pour les lignes non correspondantes sont libérés après que MySQL a évalué la condition WHERE.

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

Il existe également d'autres effets de l'utilisation du niveau d'isolement READ COMMITTED ou de l'activation de innodb_locks_unsafe_for_binlog: les verrous d'enregistrement pour les lignes non correspondantes sont libérés après que MySQL a évalué la condition WHERE.

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.

De plus, l'index n'est pas verrouillé non plus lorsque la table proc_warnings est vide

La base de données ne peut pas verrouiller les enregistrements dans l'index s'il n'y en a pas.

De plus, l'index n'est pas verrouillé lorsque ... la table day_position contient moins de lignes (soit cent lignes).

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.

JM Hicks
la source
La WHEREcondition 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 la proc_warningstable. Je pense que je vais déposer un bug sur MySQL. Merci de votre aide.
vitalidze
Je ne m'attendrais pas à ce qu'ils évitent le comportement de verrouillage non plus. Je m'attendrais à ce qu'il se verrouille parce que je pense que la documentation dit que c'est ce qui est attendu, que ce soit ou non comme cela que nous voudrions qu'il traite la requête. Je m'attendrais simplement à ce que le fait de se débarrasser du problème de performances empêche la requête simultanée de se bloquer pendant une durée aussi longue (500+ secondes).
JM Hicks
Bien que votre {WHERE} semble pouvoir être utilisé pendant le traitement de jointure pour limiter les lignes incluses dans le calcul de jointure, je ne vois pas comment votre clause {WHERE} pourrait être évaluée par ligne verrouillée jusqu'à ce que l'ensemble des jointures soit calculé également. Cela dit, pour notre analyse, je suppose que vous avez raison de soupçonner "La condition WHERE est évaluée à la fin de la requête". Pourtant, cela m'amène à la même conclusion générale, à savoir que les performances doivent être résolues, puis le degré apparent de simultanéité augmentera proportionnellement.
JM Hicks
N'oubliez pas que les index appropriés peuvent potentiellement éliminer toute analyse complète de la table qui se produit sur la table proc_warnings. Pour que cela se produise, nous avons besoin que l'optimiseur de requêtes fonctionne correctement pour nous, et nous avons besoin que nos index, requêtes et données fonctionnent correctement avec lui. Les valeurs des paramètres doivent évaluer à la fin des lignes de la table cible qui ne se chevauchent pas entre les deux requêtes. Les index doivent fournir à l'optimiseur de requête un moyen de rechercher efficacement ces lignes. Nous avons besoin de l'optimiseur pour réaliser cette efficacité de recherche potentielle et sélectionner un tel plan.
JM Hicks
Si tout va bien entre les valeurs des paramètres, les index, les résultats non chevauchants dans la table proc_warnings et la sélection du plan d'optimiseur, même si des verrous peuvent être générés pendant la durée nécessaire à l'exécution de la requête pour chaque thread, ces verrous, sinon qui se chevauchent, n'entrera pas en conflit avec les demandes de verrouillage des autres threads.
JM Hicks
3

Je peux voir comment READ_COMMITTED peut provoquer cette situation.

READ_COMMITTED permet trois choses:

  • Visibilité des modifications validées par d'autres transactions à l'aide du niveau d'isolement READ_COMMITTED .
  • Lectures non répétables: transaction effectuant la même récupération avec la possibilité d'obtenir un résultat différent à chaque fois.
  • Fantômes: les transactions peuvent avoir des lignes qui n'apparaissent pas auparavant.

Cela crée un paradigme interne pour la transaction elle-même car la transaction doit maintenir le contact avec:

  • Pool de tampons InnoDB (alors que la validation n'est toujours pas vidée)
  • Clé primaire de la table
  • Peut-être
    • le tampon d'écriture double
    • Annuler l'espace disque logique
  • Représentation picturale

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

    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;

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_locksvous fourni dans la question

| 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' |

À l'exception de lock_id, lock_trx_idle 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:

RolandoMySQLDBA
la source
J'ai lu des choses que vous décrivez dans les documents MySQL. Mais pour moi, la principale caractéristique de READ COMMITTED est la façon dont il traite les verrous . Il devrait libérer les verrous d'index des lignes non correspondantes, mais ce n'est pas le cas.
vitalidze
Si une seule instruction SQL est annulée à la suite d'une erreur, certains des verrous définis par l'instruction peuvent être conservés. Cela se produit car InnoDB stocke les verrous de ligne dans un format tel qu'il ne peut plus savoir quel verrou a été défini par quelle instruction: dev.mysql.com/doc/refman/5.5/en/innodb-deadlock-detection.html
RolandoMySQLDBA
Veuillez noter que j'ai mentionné la possibilité de deux lignes existant dans la même page pour le verrouillage (Voir Look back at information_schema.innodb_locks you supplied in the Question)
RolandoMySQLDBA
À propos de la restauration d'une seule déclaration - Je comprends que si une seule déclaration échoue dans une seule transaction, elle peut toujours contenir les verrous. C'est bon. Ma grande question est de savoir pourquoi il ne libère pas de verrous de ligne non correspondants après avoir traité avec succès l' DELETEinstruction.
vitalidze
Avec deux verrous terminés, un doit être annulé. Il est possible que des verrous persistent. THÉORIE DE TRAVAIL: la transaction qui a annulé peut réessayer et peut rencontrer un ancien verrou de la transaction précédente qui l'a détenu.
RolandoMySQLDBA
2

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:

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;

Le SELECT équivalent est:

SELECT pw.id
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=16 AND dp.dirty_data=1;

Si vous regardez son explication, vous verrez que le plan d'exécution commence par le proc_warningstableau. 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=1et de les joindre sur la proc_warningstable.

C'est-à-dire que vous devrez patcher l'un des indices:

ALTER TABLE `day_position`
 DROP INDEX `day_position__id`,
 ADD INDEX `day_position__id`
   USING BTREE (`ivehicle_day_id`, `dirty_data`, `transaction_id`);

et réécrivez la requête de suppression:

DELETE pw
FROM (
  SELECT DISTINCT dp.transaction_id
  FROM ivehicle_days vd
  JOIN day_position dp ON dp.ivehicle_day_id = vd.id
  WHERE vd.ivehicle_id=? AND dp.dirty_data=1
) as tr_id
JOIN proc_warnings pw ON pw.transaction_id = tr_id.transaction_id;
newtover
la source
Malheureusement, cela n'aide pas, c'est-à-dire que les lignes sont proc_warningstoujours verrouillées. Merci quand même.
vitalidze
2

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:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
DELETE c FROM child c
INNER JOIN parent p ON
    p.id = c.parent_id
WHERE p.id = 1;

Vous pouvez vérifier le niveau d'isolement dans lequel vous vous trouvez en interrogeant

SELECT @@tx_isolation;
Franck
la source
Ce n'est pas vrai. Pourquoi le SET AUTOCOMMIT=0devrait 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 suivante START TRANSACTIONou BEGINn'est pas nécessaire. Mon but de désactiver l'autocommit est de laisser la transaction ouverte après l' DELETEexécution de l' instruction.
vitalidze
1
@SqlKiwi c'était le moyen de modifier ce post, et c'était celui à commenter ;-)
jcolebrand