web-dev-qa-db-fra.com

(Deadlock) Sélectionner la requête provoque une bloquer la requête d'insertion dans la transaction

Base de données: MySQL

J'ai une application en cours d'exécution avec des transactions concurrentielles. J'ai remarqué qu'il y avait tellement d'erreurs liées liées à l'impasse.

L'erreur dit

SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction.

Enquêter sur une impasse

J'ai essayé de lire la trace de la pile de mon application et de son rapport de SHOW ENGINE INNODB STATUS. J'ai pu reproduire l'impasse dans un scénario simple, mais je ne comprends pas pourquoi l'impasse se produit-elle. Vous trouverez ci-dessous les informations détaillées:

DDL

CREATE TABLE `tg_users` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `tg_user_id` bigint(20) unsigned NOT NULL,
  `username` varchar(255) CHARACTER SET utf8 DEFAULT NULL,
  `first_name` varchar(255) COLLATE utf8mb4_unicode_520_ci NOT NULL,
  `last_name` varchar(255) COLLATE utf8mb4_unicode_520_ci DEFAULT NULL,
  `photo` bigint(20) unsigned DEFAULT NULL,
  `group_msg_count` bigint(20) unsigned NOT NULL DEFAULT '0',
  `private_msg_count` bigint(20) unsigned NOT NULL DEFAULT '0',
  `is_bot` enum('0','1') CHARACTER SET utf8 NOT NULL DEFAULT '0',
  `created_at` datetime NOT NULL,
  `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `tg_user_id` (`tg_user_id`),
  KEY `username` (`username`),
  KEY `first_name` (`first_name`),
  KEY `last_name` (`last_name`),
  KEY `group_msg_count` (`group_msg_count`),
  KEY `private_msg_count` (`private_msg_count`),
  KEY `created_at` (`created_at`),
  KEY `updated_at` (`updated_at`),
  KEY `photo` (`photo`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;

Logique:

  1. Sélectionnez les informations utilisateur à partir de la base de données basée sur tg_user_id.
  2. Si l'utilisateur n'a pas été stocké dans la base de données, insérez-le.
  3. Si les données de ces données existent, puis comparez et mettez à jour.

Dans ce cas, je me concentre sur l'insertion du scénario.

Juste pour souligner les choses qui peuvent être importantes pour vérifier:

  1. tg_user_id est une clé unique.
  2. La connexion 1 et la connexion 2 ont différents tg_user_id Sur ses requêtes (je pense donc que le verrou de la ligne n'est pas pertinent car ils travaillent avec une ligne différente).

Étapes de reproduction:

- (Étape 1) Ouvrez la connexion 1 et sélectionnez la requête. (Identité UNIQ: 341292662)

START TRANSACTION;
-- Query OK, 0 rows affected (0.00 sec)

SELECT 
`id`,`username`,`first_name`,`last_name`,`photo`,`group_msg_count`,`private_msg_count` 
FROM `tg_users` WHERE `tg_user_id` = '341292662' FOR UPDATE;
-- Query OK, 0 rows affected (0.00 sec)

Gardez la connexion 1 ouverte, créez une connexion 2.

- (Étape 2) Ouvrez la connexion 2 et sélectionnez la requête. (Identité UNIQ: 239302521)

START TRANSACTION;
-- Query OK, 0 rows affected (0.00 sec)

SELECT
`id`,`username`,`first_name`,`last_name`,`photo`,`group_msg_count`,`private_msg_count`
FROM `tg_users` WHERE `tg_user_id` = '239302521' FOR UPDATE;
-- Query OK, 0 rows affected (0.00 sec)

- (Étape 3) Retour à la connexion 1 et insérer (pourquoi bloque-t-il?) (Identité UNIQ: 341292662)

INSERT INTO `tg_users`
(`tg_user_id`,`username`,`first_name`,`last_name`,`photo`,`group_msg_count`,`private_msg_count`,`is_bot`,`created_at`)
VALUES
('341292662', 'derido', 'Derido', 'Novelium', NULL, '0', '0', '0', NOW())
ON DUPLICATE KEY UPDATE `id`=LAST_INSERT_ID(`id`);
-- Now the insert query is blocking

- (Étape 4) Retour à la connexion 2 et insérer (pourquoi il obtient une impasse?) (Identité UNIQ: 239302521)

INSERT INTO `tg_users`
(`tg_user_id`,`username`,`first_name`,`last_name`,`photo`,`group_msg_count`,`private_msg_count`,`is_bot`,`created_at`)
VALUES
('239302521', 'tomorimo', 'Tomorimo', 'Avede', NULL, '0', '0', '0', NOW())
ON DUPLICATE KEY UPDATE `id`=LAST_INSERT_ID(`id`);
-- ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

Des questions

  1. Pourquoi la requête à l'étape 3 est-elle bloquée? Même si la rangée à l'étape 3 n'est pas impliquée dans la connexion 2 (qui émet un IX verrou).
  2. Pourquoi l'impasse se produit-elle?
  3. Que puis-je faire pour empêcher l'impasse?

Rapport de blocage de l'état de montrage InnoDb Statut

------------------------
LATEST DETECTED DEADLOCK
------------------------
2020-08-03 22:35:26 0x9e08eb40
*** (1) TRANSACTION:
TRANSACTION 293921, ACTIVE 24 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1080, 2 row lock(s), undo log entries 1
MySQL thread id 6309, OS thread handle 2406095680, query id 41333 192.168.50.1 ammarfaizi2 update
INSERT INTO `tg_users`
(`tg_user_id`,`username`,`first_name`,`last_name`,`photo`,`group_msg_count`,`private_msg_count`,`is_bot`,`created_at`)
VALUES
('341292662', 'derido', 'Derido', 'Novelium', NULL, '0', '0', '0', NOW())
ON DUPLICATE KEY UPDATE `id`=LAST_INSERT_ID(`id`)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 10236 page no 4 n bits 72 index tg_user_id of table `test`.`tg_users` trx id 293921 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

*** (2) TRANSACTION:
TRANSACTION 293922, ACTIVE 15 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1080, 2 row lock(s), undo log entries 1
MySQL thread id 6308, OS thread handle 2651384640, query id 41334 192.168.50.1 ammarfaizi2 update
INSERT INTO `tg_users`
(`tg_user_id`,`username`,`first_name`,`last_name`,`photo`,`group_msg_count`,`private_msg_count`,`is_bot`,`created_at`)
VALUES
('239302521', 'tomorimo', 'Tomorimo', 'Avede', NULL, '0', '0', '0', NOW())
ON DUPLICATE KEY UPDATE `id`=LAST_INSERT_ID(`id`)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 10236 page no 4 n bits 72 index tg_user_id of table `test`.`tg_users` trx id 293922 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 10236 page no 4 n bits 72 index tg_user_id of table `test`.`tg_users` trx id 293922 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

*** WE ROLL BACK TRANSACTION (2)
3
Ammar Faizi

Lorsque vous exécutez les deux premiers SELECTS MySQL crée deux verrouillages X sur Supremum pseudo-enregistrement dans TG_USER_ID car les deux ID (341292662 et 239302521) en dehors de la plage existante. Cela ressemble à un bug. Lorsque vous exécutez des insertions les deux, essayez d'obtenir l'intention d'intention de verrouiller le même enregistrement, mais ils sont bloqués par des verrous précédents.

Éditer

Confirmé comme bug : https://bugs.mysql.com/bug.php?id=25847

Cependant, nous avons des solutions de contournement laids pour que ce scénario de transaction fonctionne: https://stackoverflow.com/questions/17068686/how-do-i-i-i-lock-on-an-innodb-row-that-doesnt-exist -yet

1
NikitaSerbskiy

Une sélection ... pour la mise à jour;

Verrouille votre table

Pour les enregistrements d'index Les rencontres de recherche, verrouillent les lignes et toutes les entrées d'index associées, comme si vous avez publié une déclaration de mise à jour pour ces lignes. D'autres transactions sont bloquées de la mise à jour de ces lignes, de la section Sélectionner ... pour partager ou de lire les données dans certains niveaux d'isolation des transactions. Les lectures cohérentes ignorent les verrouillages définis sur les enregistrements existants dans la vue en lecture. (Les anciennes versions d'un enregistrement ne peuvent pas être verrouillées; ils sont reconstruits en appliquant des journaux d'annulation sur une copie en mémoire de l'enregistrement.)

voir https://dev.mysql.com/doc/refman/8.0/fr/innodb-ocking-reads.html

Entrez cela, car il n'est utilisé que pour les lignes sélectionnées, qui seront mis à jour dans le prochain sstep.

0
nbk

Simplifier le DDL:

PRIMARY KEY (`id`),
UNIQUE KEY `tg_user_id` (`tg_user_id`),

->

PRIMARY KEY (`tg_user_id`),

(et se débarrasser de id). De cette façon, il y aura (peut-être) moins de serrures prises et moins de complexité globale. (ATTENTION: Si vous JOIN sur id, de telles jointures auront besoin de changer.)

Cela se débarrasse également de Last_Insert_ID (id), ce qui semble faire partie de l'impasse.

0
Rick James