Comment implémenter correctement le verrouillage optimiste dans MySQL

13

Comment implémenter correctement le verrouillage optimiste dans MySQL?

Notre équipe a déduit que nous devons faire # 4 ci-dessous, sinon il y a un risque qu'un autre thread puisse mettre à jour la même version de l'enregistrement, mais nous aimerions valider que c'est la meilleure façon de le faire.

  1. Créez un champ de version sur la table que vous souhaitez utiliser un verrouillage optimiste pour par exemple le nom de colonne = "version"
  2. Sur les sélections, assurez-vous d'inclure la colonne de version et notez la version
  3. Lors d'une mise à jour ultérieure de l'enregistrement, l'instruction de mise à jour doit émettre "où version = X" où X est la version que nous avons reçue dans # 2 et définir le champ de version pendant cette instruction de mise à jour sur X + 1.
  4. Effectuez une SELECT FOR UPDATEsur l'enregistrement que nous allons mettre à jour afin de sérialiser qui peut apporter des modifications à l'enregistrement que nous essayons de mettre à jour.

Pour clarifier, nous essayons d'empêcher deux threads qui sélectionnent le même enregistrement dans la même fenêtre de temps où ils saisissent la même version de l'enregistrement de se remplacer les uns les autres s'ils devaient essayer de mettre à jour l'enregistrement en même temps. Nous pensons que si nous ne faisons pas # 4, il y a une chance que si les deux threads entrent leurs transactions respectives en même temps (mais n'ont pas encore publié leurs mises à jour), quand ils vont mettre à jour, le deuxième thread qui utilisera la MISE À JOUR ... où version = X fonctionnera sur les anciennes données.

Avons-nous raison de penser que nous devons effectuer ce verrouillage pessimiste lors de la mise à jour, même si nous utilisons des champs de version / verrouillage optimiste?

Les meilleures pratiques
la source
Quel est le problème? Vous augmentez le numéro de version avec votre MISE À JOUR, puis la deuxième MISE À JOUR échouera car le numéro de version n'est pas le même que lorsqu'il a été lu - ce que vous voulez.
AndreKR
Êtes-vous sûr? Il n'est pas clair que, sauf si vous définissez le niveau d'isolement des transactions sur un paramètre spécifique, vous verriez en fait la mise à jour des autres threads. Si vous entrez tous les deux la transaction en même temps, le deuxième thread peut très bien voir les anciennes données quand il va faire la mise à jour. MySQL n'est pas aussi robuste dans l'arène ACID que le dit Oracle, recherchant ainsi le meilleur moyen d'implémenter un verrouillage optimiste dans MySQL qui empêchera les lectures / mises à jour incorrectes.
BestPractices
Mais la transaction échouera quand même pendant la validation, non?
AndreKR
Les indications sont que l'on voudrait faire une sélection pour la mise à jour afin de faire face à cette situation: dev.mysql.com/doc/refman/5.0/en/innodb-consistent-read.html
BestPractices
@BestPractices Vous avez besoin d' un SELECT ... FOR UPDATE verrouillage optimiste ou par version de ligne, pas des deux. Voir détail en réponse.
Craig Ringer

Réponses:

17

Votre développeur se trompe. Vous avez besoin d' une version SELECT ... FOR UPDATE ou d'une version en ligne, pas des deux.

Essayez-le et voyez. Ouvrez trois sessions de MySQL (A), (B)et (C)à la même base de données.

En (C)cause:

CREATE TABLE test(
    id integer PRIMARY KEY,
    data varchar(255) not null,
    version integer not null
);
INSERT INTO test(id,data,version) VALUES (1,'fred',0);
BEGIN;
LOCK TABLES test WRITE;

Dans les deux (A)et (B)émettez un UPDATEqui teste et définit la version de la ligne, en changeant le winnertexte dans chacun afin que vous puissiez voir quelle session est laquelle:

-- In (A):

BEGIN;
UPDATE test SET data = 'winnerA',
            version = version + 1
WHERE id = 1 AND version = 0;

-- in (B):

BEGIN;
UPDATE test SET data = 'winnerB',
            version = version + 1
WHERE id = 1 AND version = 0;

Maintenant (C), UNLOCK TABLES;pour libérer le verrou.

(A)et (B)courra pour le verrouillage de la ligne. L'un d'eux gagnera et obtiendra le cadenas. L'autre bloquera la serrure. Le gagnant qui a obtenu le verrou procédera au changement de ligne. En supposant que (A)c'est le gagnant, vous pouvez maintenant voir la ligne modifiée (toujours non validée, donc non visible pour les autres transactions) avec a SELECT * FROM test WHERE id = 1.

Maintenant COMMITdans la session gagnante, disons (A).

(B)va obtenir le verrou et procéder à la mise à jour. Cependant, la version ne correspond plus, elle ne modifiera donc aucune ligne, comme indiqué par le résultat du nombre de lignes. Un seul a UPDATEeu un effet, et l'application cliente peut clairement voir laquelle a UPDATEréussi et qui a échoué. Aucun verrouillage supplémentaire n'est nécessaire.

Voir les journaux de session sur pastebin ici . J'ai utilisé mysql --prompt="A> "etc pour faire facilement la différence entre les sessions. J'ai copié et collé la sortie entrelacée dans une séquence temporelle, donc ce n'est pas une sortie totalement brute et il est possible que j'aurais pu faire des erreurs en la copiant et en la collant. Testez-le vous-même pour voir.


Si vous aviez pas ajouté un champ de version de la ligne, alors vous devez SELECT ... FOR UPDATEêtre en mesure d'assurer de manière fiable la commande.

Si vous y réfléchissez, a SELECT ... FOR UPDATEest complètement redondant si vous faites immédiatement un UPDATEsans réutiliser les données de la SELECT, ou si vous utilisez le versionnage de ligne. Le UPDATEprendra une serrure de toute façon. Si quelqu'un d'autre met à jour la ligne entre votre lecture et votre écriture ultérieure, votre version ne correspondra plus, votre mise à jour échouera. Voilà comment fonctionne le verrouillage optimiste.

Le but de SELECT ... FOR UPDATE:

  • Pour gérer l'ordre des verrous afin d'éviter les blocages; et
  • Pour étendre la portée d'un verrou de ligne lorsque vous souhaitez lire les données d'une ligne, modifiez-les dans l'application et écrivez une nouvelle ligne basée sur l'original sans avoir à utiliser l' SERIALIZABLEisolation ou la gestion des versions de ligne.

Vous n'avez pas besoin d'utiliser à la fois le verrouillage optimiste (version de ligne) et SELECT ... FOR UPDATE. Utilise l'un ou l'autre.

Craig Ringer
la source
Merci Craig. Vous aviez raison - le développeur s'est trompé. Merci d'avoir exécuté ce test.
BestPractices
Qu'en est-il du serveur SQL? Y a-t-il toujours un verrou acquis sur la ligne mise à jour indépendamment du niveau d'isolement des transactions?
plalx
@plalx Eh bien, que dit la documentation? Que se passe-t-il si vous exécutez un test interactif comme celui-ci?
Craig Ringer
@CraigRinger, que se passera-t-il si B obtient le verrou avant A commit mais après A update?
MengT
1
@MengT Il ne peut pas, c'est pourquoi c'est un verrou.
Craig Ringer
0
UPDATE tbl SET owner = $me,
               id = LAST_INSERT_ID(id)
    WHERE owner = ''
    LIMIT 1;
$id = SELECT LAST_INSERT_ID();
Do some stuff (arbitrarily long time)...;
UPDATE  tbl SET owner = '' WHERE id = $id;

Aucun verrou (pas de table, pas de transaction) nécessaire ou même souhaité:

  • UPDATE est atomique
  • LAST_INSERT_ID () est spécifique à la session, donc thread-safe.
Rick James
la source