web-dev-qa-db-fra.com

MySQL: Transactions vs tables de verrouillage

Je suis un peu confus avec les transactions vs les tables de verrouillage pour assurer l'intégrité de la base de données et pour m'assurer que SELECT et UPDATE restent synchronisés et qu'aucune autre connexion n'interfère avec elle. J'ai besoin de:

SELECT * FROM table WHERE (...) LIMIT 1

if (condition passes) {
   // Update row I got from the select 
   UPDATE table SET column = "value" WHERE (...)

   ... other logic (including INSERT some data) ...
}

Je dois m'assurer qu'aucune autre requête n'interférera et n'effectuera la même opération SELECT (en lisant l'ancienne valeur avant que la connexion ne termine la mise à jour de la ligne.

Je sais que je peux par défaut sur LOCK TABLES table pour vous assurer que 1 seule connexion est en train de le faire à la fois, et la déverrouiller lorsque j'ai terminé, mais cela semble excessif. Est-ce que le fait d'encapsuler cela dans une transaction ferait la même chose (en s'assurant qu'aucune autre connexion ne tente le même processus pendant qu'une autre est en cours de traitement)? Ou serait un SELECT ... FOR UPDATE ou SELECT ... LOCK IN SHARE MODE être meilleur?

98
Ryan

Le verrouillage des tables empêche les autres utilisateurs de la base de données d’affecter les lignes/tables que vous avez verrouillées. Mais les verrous, en eux-mêmes, ne garantissent PAS que votre logique ressort dans un état cohérent.

Pensez à un système bancaire. Lorsque vous payez une facture en ligne, au moins deux comptes sont concernés par la transaction: Votre compte, d'où provient l'argent. Et le compte du destinataire dans lequel l'argent est transféré. Et le compte de la banque, sur lequel ils déposeront avec plaisir tous les frais de service facturés pour la transaction. Etant donné (comme chacun le sait aujourd'hui) que les banques sont extraordinairement stupides, supposons que leur système fonctionne comme suit:

$balance = "GET BALANCE FROM your ACCOUNT";
if ($balance < $amount_being_paid) {
    charge_huge_overdraft_fees();
}
$balance = $balance - $amount_being paid;
UPDATE your ACCOUNT SET BALANCE = $balance;

$balance = "GET BALANCE FROM receiver ACCOUNT"
charge_insane_transaction_fee();
$balance = $balance + $amount_being_paid
UPDATE receiver ACCOUNT SET BALANCE = $balance

Désormais, sans verrouillage ni transaction, ce système est vulnérable à diverses conditions de concurrence. La plus importante d'entre elles est le paiement multiple effectué sur votre compte ou sur le compte du destinataire en parallèle. Bien que le solde de votre code soit récupéré et que vous exécutiez huge_overdraft_fees (), etc., il est tout à fait possible qu'un autre paiement utilise le même type de code en parallèle. Ils récupéreront votre solde (par exemple, 100 USD), effectueront leurs transactions (retirez les 20 USD que vous payez et les 30 USD avec lesquels ils vous foutent), et maintenant les deux chemins de code ont deux soldes différents: 80 USD et plus. 70 $. En fonction de celles qui se terminent en dernier, vous obtiendrez l'un ou l'autre de ces deux soldes dans votre compte, au lieu des 50 $ que vous auriez dû avoir (100 $ - 20 $ - 30 $). Dans ce cas, "erreur de la banque en votre faveur".

Maintenant, disons que vous utilisez des verrous. Le paiement de votre facture (20 $) frappe d'abord le tuyau, il gagne et verrouille votre enregistrement de compte. Maintenant, vous avez un usage exclusif et pouvez déduire les 20 € du solde, et réécrivez le nouveau solde en toute tranquillité ... et votre compte se termine avec 80 € comme prévu. Mais ... euh ... vous essayez de mettre à jour le compte du destinataire, qui est verrouillé, et verrouillé plus longtemps que le code ne le permet, chronométrant votre transaction ... Nous avons affaire à des banques stupides, alors au lieu d'avoir l'erreur appropriée manipulation, le code tire juste une exit(), et vos 20 $ disparaissent dans une bouffée d’électrons. Maintenant, vous avez 20 dollars, vous devez toujours 20 dollars au destinataire et votre téléphone est repris.

Alors ... entrez les transactions. Vous commencez une transaction, vous débitez votre compte de 20 $, vous essayez de créditer le destinataire de 20 $ ... et quelque chose explose à nouveau. Mais cette fois, au lieu de exit(), le code ne peut que faire rollback, et pouf, vos 20 € sont ajoutés à votre compte comme par magie.

En fin de compte, cela se résume à ceci:

Les verrous empêchent toute autre personne d'interférer avec les enregistrements de base de données avec lesquels vous traitez. Les transactions empêchent les erreurs "ultérieures" d'interférer avec les actions "antérieures" que vous avez effectuées. Ni les uns ni les autres ne peuvent garantir que tout se passera bien. Mais ensemble, ils le font.

dans la leçon de demain: La joie des impasses.

156
Marc B

Vous voulez une SELECT ... FOR UPDATE ou SELECT ... LOCK IN SHARE MODE à l'intérieur d'une transaction, comme vous l'avez dit, puisque normalement les SELECT, peu importe si elles sont dans une transaction ou non, ne verrouillent pas une table. Le choix que vous choisirez dépendra de la question de savoir si vous voulez que d'autres transactions puissent lire cette ligne pendant que votre transaction est en cours.

http://dev.mysql.com/doc/refman/5.0/en/innodb-locking-reads.html

START TRANSACTION WITH CONSISTENT SNAPSHOT ne fera pas l'affaire pour vous, car d'autres transactions peuvent toujours arriver et modifier cette ligne. Ceci est mentionné en haut du lien ci-dessous.

Si d'autres sessions mettent à jour simultanément la même table, vous [...] pouvez voir la table dans un état qui n'a jamais existé dans la base de données.

http://dev.mysql.com/doc/refman/5.0/en/innodb-consistent-read.html

14
Alison R.

J'ai eu un problème similaire en essayant un IF NOT EXISTS ... et ensuite effectuer un INSERT qui a provoqué une situation de concurrence critique lorsque plusieurs threads mettaient à jour la même table.

J'ai trouvé la solution au problème ici: Comment écrire des requêtes INSERT IF NOT EXISTS en SQL standard

Je me rends compte que cela ne répond pas directement à votre question, mais le même principe de vérification et d'insertion en tant que déclaration unique est très utile; vous devriez pouvoir le modifier pour effectuer votre mise à jour.

6
Tony

Les concepts de transaction et les verrous sont différents. Cependant, transaction a utilisé des verrous pour l'aider à suivre les principes ACID. Si vous souhaitez que la table empêche les autres personnes de lire/écrire en même temps lorsque vous êtes en lecture/écriture, vous avez besoin d'un verrou pour le faire. Si vous voulez vous assurer de l'intégrité et de la cohérence des données, vous feriez mieux d'utiliser des transactions. Je pense à des concepts mixtes de niveaux d’isolement dans les transactions avec des verrous. Veuillez rechercher les niveaux d'isolation des transactions, SERIALIZE doit correspondre au niveau souhaité.

4
tczhaodachuan

Vous êtes confus avec lock & transaction. Ce sont deux choses différentes dans RMDB. Le verrouillage empêche les opérations simultanées lorsque la transaction est axée sur l'isolation des données Découvrez this excellent article pour la clarification et une solution élégante.

2
David

J'utiliserais un

START TRANSACTION WITH CONSISTENT SNAPSHOT;

pour commencer, et un

COMMIT;

pour finir avec.

Tout ce que vous faites entre les deux est isolé des autres utilisateurs de votre base de données si votre moteur de stockage prend en charge les transactions (qui est InnoDB).

1
Martin Schapendonk