Comment éviter le blocage mysql trouvé lors de la tentative de verrouillage; essayez de redémarrer la transaction '

286

J'ai une table innoDB qui enregistre les utilisateurs en ligne. Il est mis à jour à chaque actualisation de page par un utilisateur pour garder une trace des pages sur lesquelles il se trouve et de sa dernière date d'accès au site. J'ai ensuite un cron qui s'exécute toutes les 15 minutes pour SUPPRIMER les anciens enregistrements.

J'ai trouvé un «Deadlock» en essayant de verrouiller; essayez de redémarrer la transaction 'pendant environ 5 minutes la nuit dernière et cela semble être le cas lors de l'exécution d'INSERT dans ce tableau. Quelqu'un peut-il suggérer comment éviter cette erreur?

=== EDIT ===

Voici les requêtes en cours d'exécution:

Première visite sur le site:

INSERT INTO onlineusers SET
ip = 123.456.789.123,
datetime = now(),
userid = 321,
page = '/thispage',
area = 'thisarea',
type = 3

Sur chaque page, actualisez:

UPDATE onlineusers SET
ips = 123.456.789.123,
datetime = now(),
userid = 321,
page = '/thispage',
area = 'thisarea',
type = 3
WHERE id = 888

Cron toutes les 15 minutes:

DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND

Il effectue ensuite quelques décomptes pour enregistrer certaines statistiques (par exemple: membres en ligne, visiteurs en ligne).

David
la source
Pouvez-vous fournir plus de détails sur la structure de la table? Existe-t-il des index cluster ou non cluster?
Anders Abel
13
dev.mysql.com/doc/refman/5.1/en/innodb-deadlocks.html - L'exécution de "show engine innodb status" fournirait des diagnostics utiles.
Martin
Ce n'est pas une bonne pratique de faire une écriture de base de données synchrone lorsque les utilisateurs marchent sur une page. la bonne façon de le faire est de le garder en mémoire tel que memcache ou une file d'attente rapide et d'écrire sur db avec cron.
Nir

Réponses:

292

Une astuce simple qui peut aider avec la plupart des blocages est de trier les opérations dans un ordre spécifique.

Vous obtenez un blocage lorsque deux transactions tentent de verrouiller deux verrous dans des ordres opposés, c'est-à-dire:

  • connexion 1: clé de verrouillage (1), clé de verrouillage (2);
  • connexion 2: clé de verrouillage (2), clé de verrouillage (1);

Si les deux s'exécutent en même temps, la connexion 1 verrouille la clé (1), la connexion 2 verrouille la clé (2) et chaque connexion attend que l'autre relâche la clé -> blocage.

Maintenant, si vous avez modifié vos requêtes de telle sorte que les connexions verrouillent les clés dans le même ordre, c'est-à-dire:

  • connexion 1: clé de verrouillage (1), clé de verrouillage (2);
  • connexion 2: clé de verrouillage ( 1 ), clé de verrouillage ( 2 );

il sera impossible d'obtenir une impasse.

Voici donc ce que je propose:

  1. Assurez-vous qu'aucune autre requête ne verrouille l'accès à plusieurs clés à la fois, à l'exception de l'instruction delete. si vous le faites (et je soupçonne que vous le faites), commandez leur OERE en (k1, k2, .. kn) dans l'ordre croissant.

  2. Corrigez votre instruction de suppression pour qu'elle fonctionne dans l'ordre croissant:

Changement

DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND

À

DELETE FROM onlineusers WHERE id IN (SELECT id FROM onlineusers
    WHERE datetime <= now() - INTERVAL 900 SECOND order by id) u;

Une autre chose à garder à l'esprit est que la documentation mysql suggère qu'en cas de blocage, le client devrait réessayer automatiquement. vous pouvez ajouter cette logique à votre code client. (Disons, 3 tentatives sur cette erreur particulière avant d'abandonner).

Omry Yadan
la source
2
Si j'ai Transaction (autocommit = false), une exception de blocage est levée. Est-ce qu'il suffit de réessayer la même instruction.executeUpdate () ou la transaction entière est maintenant gimped et devrait être annulée + réexécuter tout ce qui y était exécuté?
Qui le
5
si vous avez activé les transactions, c'est tout ou rien. si vous aviez une exception de quelque nature que ce soit, il est garanti que l'ensemble de la transaction n'a eu aucun effet. dans ce cas, vous voudrez redémarrer le tout.
Omry Yadan
4
Une suppression basée sur une sélection sur une grande table est très lente qu'une simple suppression
Thermech
3
Merci beaucoup, mec. L'astuce «Trier les instructions» a corrigé mes problèmes de blocage.
Miere
4
@OmryYadan D'après ce que je sais, dans MySQL, vous ne pouvez pas sélectionner dans une sous-requête de la même table dans laquelle vous effectuez la MISE À JOUR. dev.mysql.com/doc/refman/5.7/en/update.html
artaxerxe
73

Le blocage se produit lorsque deux transactions attendent l'une de l'autre pour acquérir un verrou. Exemple:

  • Tx 1: verrouiller A, puis B
  • Tx 2: verrouiller B, puis A

Il existe de nombreuses questions et réponses sur les blocages. Chaque fois que vous insérez / mettez à jour / supprimez une ligne, un verrou est acquis. Pour éviter un blocage, vous devez ensuite vous assurer que les transactions simultanées ne mettent pas à jour la ligne dans un ordre qui pourrait entraîner un blocage. De manière générale, essayez d'acquérir le verrou toujours dans le même ordre, même dans une transaction différente (par exemple toujours la table A d'abord, puis la table B).

Une autre raison de blocage dans la base de données peut être l' absence d'index . Lorsqu'une ligne est insérée / mise à jour / suppression, la base de données doit vérifier les contraintes relationnelles, c'est-à-dire s'assurer que les relations sont cohérentes. Pour ce faire, la base de données doit vérifier les clés étrangères dans les tables associées. Cela peut entraîner l'acquisition d'un autre verrou que la ligne modifiée. Assurez-vous alors de toujours avoir l'index sur les clés étrangères (et bien sûr les clés primaires), sinon cela pourrait entraîner un verrou de table au lieu d'un verrou de ligne . Si le verrouillage de table se produit, le conflit de verrouillage est plus élevé et la probabilité de blocage augmente.

ewernli
la source
3
Alors peut-être que mon problème est que l'utilisateur a actualisé la page et déclenché ainsi une MISE À JOUR d'un enregistrement en même temps que le cron essaie d'exécuter une SUPPRESSION sur l'enregistrement. Cependant, je reçois l'erreur sur INSERTS, de sorte que le cron ne serait pas SUPPRIMER les enregistrements qui viennent d'être créés. Alors, comment un blocage peut-il se produire sur un enregistrement qui n'a pas encore été inséré?
David
Pouvez-vous fournir un peu plus d'informations sur les tables et ce que font exactement les transactions?
ewernli
Je ne vois pas comment un blocage pourrait se produire s'il n'y a qu'une seule instruction par transaction. Aucune autre opération sur d'autres tables? Pas de clés étrangères spéciales ou de contraintes uniques? Pas de contraintes de suppression en cascade?
ewernli
non, rien de spécial ... Je suppose que c'est dû à la nature de l'utilisation de la table. une ligne est insérée / mise à jour à chaque actualisation de page d'un visiteur. Environ 1000+ visiteurs sont en même temps.
David
12

Il est probable que l'instruction delete affectera une grande partie du nombre total de lignes du tableau. Finalement, cela pourrait entraîner l'acquisition d'un verrou de table lors de la suppression. Conserver un verrou (dans ce cas, des verrous de ligne ou de page) et acquérir plus de verrous est toujours un risque de blocage. Cependant, je ne peux pas expliquer pourquoi l'instruction d'insertion conduit à une escalade de verrouillage - cela peut avoir à voir avec le fractionnement / l'ajout de pages, mais quelqu'un qui connaît mieux MySQL devra y remplir.

Pour commencer, il peut être utile d'essayer d'acquérir explicitement un verrou de table immédiatement pour l'instruction delete. Consultez les sections VERROUILLER LES TABLEAUX et Verrouiller la table .

Anders Abel
la source
6

Vous pouvez essayer de faire fonctionner ce deletetravail en insérant d'abord la clé de chaque ligne à supprimer dans une table temporaire comme ce pseudocode

create temporary table deletetemp (userid int);

insert into deletetemp (userid)
  select userid from onlineusers where datetime <= now - interval 900 second;

delete from onlineusers where userid in (select userid from deletetemp);

Le briser de cette façon est moins efficace, mais il évite d'avoir à maintenir un verrou de plage de clés pendant le delete.

Modifiez également vos selectrequêtes pour ajouter une whereclause excluant les lignes de plus de 900 secondes. Cela évite la dépendance à l'égard du travail cron et vous permet de le replanifier pour qu'il s'exécute moins souvent.

Théorie sur les blocages: je n'ai pas beaucoup d'expérience dans MySQL mais voilà ... Le deleteva contenir un verrou de plage de clés pour datetime, pour empêcher l' whereajout de lignes correspondant à sa clause au milieu de la transaction , et lorsqu'il trouvera des lignes à supprimer, il tentera d'acquérir un verrou sur chaque page qu'il modifie. Le insertva acquérir un verrou sur la page dans laquelle il est inséré, puis tenter d'acquérir le verrouillage des touches. Normalement, le insertpatient attendra patiemment que ce verrou de clé s'ouvre, mais cela bloquera si le deletetente de verrouiller la même page qu'il insertutilise, car il a deletebesoin de ce verrouillage de page et a insertbesoin de ce verrouillage de clé. Cela ne semble pas correct pour les insertions, le deleteetinsert utilisent des plages de datetime qui ne se chevauchent pas, alors peut-être que quelque chose d'autre se passe.

http://dev.mysql.com/doc/refman/5.1/en/innodb-next-key-locking.html

Brian Sandlin
la source
4

Dans le cas où quelqu'un est toujours aux prises avec ce problème:

J'ai rencontré un problème similaire où 2 demandes frappaient le serveur en même temps. Il n'y avait aucune situation comme ci-dessous:

T1:
    BEGIN TRANSACTION
    INSERT TABLE A
    INSERT TABLE B
    END TRANSACTION

T2:
    BEGIN TRANSACTION
    INSERT TABLE B
    INSERT TABLE A
    END TRANSACTION

Donc, j'étais perplexe pourquoi l'impasse se produit.

Ensuite, j'ai trouvé qu'il y avait une relation parent-enfant entre 2 tables à cause de la clé étrangère. Lorsque j'insérais un enregistrement dans la table enfant, la transaction acquérait un verrou sur la ligne de la table parent. Immédiatement après cela, j'essayais de mettre à jour la ligne parent qui déclenchait l'élévation du verrou à EXCLUSIVE. Comme la 2e transaction simultanée détenait déjà un verrou PARTAGÉ, elle provoquait un blocage.

Reportez-vous à: https://blog.tekenlight.com/2019/02/21/database-deadlock-mysql.html

chatsap
la source
Dans mon cas aussi, il semble que le problème était une relation de clé étrangère. Thanks1
Chris Prince
3

Pour les programmeurs Java utilisant Spring, j'ai évité ce problème en utilisant un aspect AOP qui réessaye automatiquement les transactions qui s'exécutent dans des blocages temporaires.

Voir @RetryTransaction Javadoc pour plus d'informations.

Archie
la source
0

J'ai une méthode, dont les éléments internes sont enveloppés dans une MySqlTransaction.

Le problème de blocage est apparu pour moi lorsque j'ai exécuté la même méthode en parallèle avec lui-même.

Il n'y a eu aucun problème lors de l'exécution d'une seule instance de la méthode.

Lorsque j'ai supprimé MySqlTransaction, j'ai pu exécuter la méthode en parallèle avec elle-même sans aucun problème.

Partageant mon expérience, je ne préconise rien.

CINCHAPPS
la source
0

cronest dangereux. Si une instance de cron ne parvient pas à se terminer avant la suivante, ils sont susceptibles de se battre.

Il serait préférable d'avoir un travail en cours d'exécution qui supprimerait certaines lignes, en mettrait en veille, puis recommencerait.

Est également INDEX(datetime)très important pour éviter les blocages.

Mais, si le test datetime inclut plus de, disons, 20% de la table, le DELETEfera une analyse de table. Les petits morceaux supprimés plus souvent constituent une solution de contournement.

Une autre raison d'aller avec des morceaux plus petits est de verrouiller moins de lignes.

Conclusion:

  • INDEX(datetime)
  • Tâche en cours d'exécution - supprimer, dormir une minute, répéter.
  • Pour vous assurer que la tâche ci-dessus n'est pas morte, ayez une tâche cron dont le seul but est de la redémarrer en cas d'échec.

Autres techniques de suppression: http://mysql.rjweb.org/doc.php/deletebig

Rick James
la source