Mysql
 sql >> Base de données >  >> RDS >> Mysql

Requête de mise à jour MySQL - La condition "où" sera-t-elle respectée sur la condition de concurrence et le verrouillage de ligne ? (php, AOP, MySQL, InnoDB)

La condition où sera respectée lors d'une situation de course, mais vous devez faire attention à la façon dont vous vérifiez pour voir qui a gagné la course.

Considérez la démonstration suivante de comment cela fonctionne et pourquoi vous devez être prudent.

Tout d'abord, configurez quelques tables minimales.

CREATE TABLE table1 (
`id` TINYINT UNSIGNED NOT NULL PRIMARY KEY,
`locked` TINYINT UNSIGNED NOT NULL,
`updated_by_connection_id` TINYINT UNSIGNED DEFAULT NULL
) ENGINE = InnoDB;

CREATE TABLE table2 (
`id` TINYINT UNSIGNED NOT NULL PRIMARY KEY
) ENGINE = InnoDB;

INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

id joue le rôle de id dans votre tableau, updated_by_connection_id agit comme assignedPhone , et locked comme reservationCompleted .

Commençons maintenant le test de course. Vous devriez avoir 2 fenêtres de ligne de commande/terminal ouvertes, connectées à mysql et utilisant la base de données où vous avez créé ces tables.

Connexion 1

start transaction;

Connexion 2

start transaction;

Connexion 1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;

Connexion 2

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;

La connexion 2 est maintenant en attente

Connexion 1

SELECT * FROM table1 WHERE id = 1;
commit;

À ce stade, la connexion 2 est libérée pour continuer et affiche ce qui suit :

Connexion 2

SELECT * FROM table1 WHERE id = 1;
commit;

Tout semble bien. On voit que oui, la clause WHERE a été respectée en situation de course.

La raison pour laquelle j'ai dit qu'il fallait faire attention, c'est parce que dans une application réelle, les choses ne sont pas toujours aussi simples. Vous POUVEZ avoir d'autres actions en cours dans la transaction, et cela peut réellement changer les résultats.

Réinitialisons la base de données avec ce qui suit :

delete from table1;
INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

Et maintenant, considérons cette situation, où un SELECT est effectué avant le UPDATE.

Connexion 1

start transaction;

SELECT * FROM table2;

Connexion 2

start transaction;

SELECT * FROM table2;

Connexion 1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;

Connexion 2

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;

La connexion 2 est maintenant en attente

Connexion 1

SELECT * FROM table1 WHERE id = 1;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

À ce stade, la connexion 2 est libérée pour continuer et affiche ce qui suit :

Ok, voyons qui a gagné :

Connexion 2

SELECT * FROM table1 WHERE id = 1;

Attends quoi? Pourquoi locked 0 et updated_by_connection_id NULL ??

C'est la prudence dont j'ai parlé. Le coupable est en fait dû au fait que nous avons fait une sélection au début. Pour obtenir le bon résultat, nous pourrions exécuter ce qui suit :

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

En utilisant SELECT ... FOR UPDATE, nous pouvons obtenir le bon résultat. Cela peut être très déroutant (comme c'était le cas pour moi, à l'origine), car un SELECT et un SELECT ... FOR UPDATE donnent deux résultats différents.

La raison pour laquelle cela se produit est due au niveau d'isolement par défaut READ-REPEATABLE . Lorsque le premier SELECT est effectué, juste après le start transaction; , un instantané est créé. Toutes les futures lectures sans mise à jour seront effectuées à partir de cet instantané.

Par conséquent, si vous SELECT naïvement juste après avoir fait la mise à jour, il extraira les informations de cet instantané d'origine, qui est avant la ligne a été mise à jour. En faisant un SELECT ... FOR UPDATE vous le forcez à obtenir les bonnes informations.

Cependant, encore une fois, dans une application réelle, cela pourrait être un problème. Supposons, par exemple, que votre demande soit encapsulée dans une transaction et qu'après avoir effectué la mise à jour, vous souhaitiez afficher certaines informations. La collecte et la sortie de ces informations peuvent être gérées par un code séparé et réutilisable, que vous ne voulez PAS jeter avec des clauses FOR UPDATE "juste au cas où". Cela entraînerait beaucoup de frustration en raison d'un verrouillage inutile.

Au lieu de cela, vous aurez envie de prendre une piste différente. Vous avez de nombreuses options ici.

Premièrement, assurez-vous de valider la transaction une fois la mise à jour terminée. Dans la plupart des cas, c'est probablement le meilleur choix, le plus simple.

Une autre option est de ne pas essayer d'utiliser SELECT pour déterminer le résultat. Au lieu de cela, vous pourrez peut-être lire les lignes affectées et les utiliser (1 ligne mise à jour contre 0 ligne mise à jour) pour déterminer si la mise à jour a été un succès.

Une autre option, et celle que j'utilise fréquemment, car j'aime garder une seule requête (comme une requête HTTP) entièrement enveloppée dans une seule transaction, est de s'assurer que la première instruction exécutée dans une transaction est soit le UPDATE ou un SELECT ... FOR UPDATE . Cela empêchera la capture de l'instantané tant que la connexion ne sera pas autorisée à se poursuivre.

Réinitialisons à nouveau notre base de données de test et voyons comment cela fonctionne.

delete from table1;
INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

Connexion 1

start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;

Connexion 2

start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;

La connexion 2 est maintenant en attente.

Connexion 1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;
SELECT * FROM table1 WHERE id = 1;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

La connexion 2 est désormais disponible.

Connexion 2

+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
|  1 |      1 |                        1 |
+----+--------+--------------------------+

Ici, vous pouvez en fait demander à votre code côté serveur de vérifier les résultats de ce SELECT et de savoir qu'il est exact, et même de ne pas passer aux étapes suivantes. Mais, pour être complet, je vais terminer comme avant.

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;
SELECT * FROM table1 WHERE id = 1;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

Vous pouvez maintenant voir que dans la connexion 2, SELECT et SELECT ... FOR UPDATE donnent le même résultat. Cela est dû au fait que l'instantané à partir duquel le SELECT lit n'a été créé qu'après la validation de la connexion 1.

Donc, revenons à votre question initiale :oui, la clause WHERE est vérifiée par l'instruction UPDATE, dans tous les cas. Cependant, vous devez être prudent avec tous les SELECT que vous pourriez faire, pour éviter de déterminer de manière incorrecte le résultat de cette UPDATE.

(Oui, une autre option consiste à modifier le niveau d'isolement de la transaction. Cependant, je n'ai pas vraiment d'expérience avec cela et les pièges qui pourraient exister, donc je ne vais pas m'y attarder.)