Pourquoi CTE est-il ouvert aux mises à jour perdues?

8

Je ne comprends pas ce que Craig Ringer voulait dire quand il a commenté:

Cette solution est sujette à des mises à jour perdues si la transaction d'insertion est annulée; il n'y a aucune vérification pour s'assurer que la MISE À JOUR a affecté toutes les lignes.

sur https://stackoverflow.com/a/8702291/14731 . Veuillez fournir un exemple de séquence d'événements (par exemple, le thread 1 fait X, le thread 2 fait Y) qui montre comment les mises à jour perdues peuvent se produire.

Gili
la source
1
Demandez-moi quelque chose à propos d'un commentaire que j'ai laissé il y a plus d'un an sur un sujet complexe ... amusant! Maintenant, je dois me rappeler quel était le problème exact. Le réexaminer maintenant.
Craig Ringer

Réponses:

14

Je pense que je voulais probablement ajouter ce commentaire sur la réponse précédente, à propos de deux déclarations distinctes. C'était il y a plus d'un an, donc je ne suis plus totalement sûr.

La requête basée sur wCTE ne résout pas vraiment le problème qu'il est censé résoudre, mais après l'avoir réexaminée plus d'un an plus tard, je ne vois pas la possibilité de mises à jour perdues dans la version wCTE.

(Notez que toutes ces solutions ne fonctionneront bien que si vous essayez de changer exactement une ligne avec chaque transaction. Dès que vous essayez de faire plusieurs changements dans une transaction, les choses deviennent compliquées en raison de la nécessité de réessayer les boucles lors des annulations. Au minimum vous devez utiliser un point de sauvegarde entre chaque modification.)

Version à deux états sous réserve de mises à jour perdues

La version qui utilise deux instructions distinctes est sujette à des mises à jour perdues à moins que l'application vérifie le nombre de lignes affectées de l' UPDATEinstruction et de l' INSERTinstruction et réessaye si les deux sont nuls.

Imaginez ce qui se passe si vous avez deux transactions READ COMMITTEDisolément.

  • TX1 exécute le UPDATE(aucun effet)
  • TX1 exécute le INSERT(insère une ligne)
  • TX2 exécute le UPDATE(aucun effet, la ligne insérée par TX1 n'est pas encore visible)
  • TX1 COMMITs.
  • TX2 exécute le INSERT, * qui obtient un nouvel instantané qui peut voir la ligne validée par TX1. La EXISTSclause renvoie true, car TX2 peut maintenant voir la ligne insérée par TX1.

TX2 n'a donc aucun effet. À moins que l'application vérifie le nombre de lignes à partir de la mise à jour et de l'insertion et réessaye si les deux signalent zéro ligne, elle ne saura pas que la transaction n'a eu aucun effet et se poursuivra joyeusement.

La seule façon dont il peut vérifier les nombres de lignes affectés est de l'exécuter en tant que deux instructions distinctes plutôt qu'en plusieurs instructions, ou d'utiliser une procédure.

Vous pouvez utiliser l' SERIALIZABLEisolement, mais vous aurez toujours besoin d'une boucle de relance pour faire face aux échecs de sérialisation.

La version wCTE protège contre le problème des mises à jour perdues car le INSERTest conditionnel à l' UPDATEaffectation de lignes, plutôt qu'à une requête distincte.

Le wCTE n'élimine pas les violations uniques

La version CTE inscriptible n'est toujours pas un upsert fiable.

Considérez deux transactions qui s'exécutent simultanément.

  • Les deux exécutent la clause VALUES.

  • Maintenant, les deux exécutent la UPDATEpartie. Puisqu'il n'y a pas de lignes correspondant à la UPDATEclause s where, les deux renvoient un ensemble de résultats vide de la mise à jour et n'apportent aucune modification.

  • Maintenant, les deux exécutent la INSERTportion. Étant donné que les UPDATElignes retournées zéro pour les deux requêtes, les deux tentent de INSERTla ligne.

On réussit. On jette une violation unique et abandonne.

Ce n'est pas une source de préoccupation pour la perte de données tant que l'application vérifie les résultats d'erreur de ses requêtes (c'est-à-dire toute application décemment écrite) et réessaie, mais cela ne rend pas la solution meilleure que les versions existantes à deux instructions. Il n'élimine pas la nécessité d'une boucle de nouvelle tentative.

L'avantage du wCTE par rapport à la version existante à deux instructions est qu'il utilise la sortie de la UPDATEpour décider si INSERT, au lieu d'utiliser une requête distincte sur la table. C'est en partie une optimisation, mais cela protège en partie contre un problème avec la version à deux instructions qui provoque des mises à jour perdues; voir ci-dessous.

Vous pouvez exécuter le wCTE de manière SERIALIZABLEisolée, mais vous obtiendrez alors simplement des échecs de sérialisation au lieu de violations uniques. Cela ne changera pas la nécessité d'une boucle de nouvelle tentative.

Le WCTE ne pas semble être vulnérable aux mises à jour perdues

Mon commentaire a suggéré que cette solution pourrait entraîner la perte de mises à jour, mais après examen, je pense que je me suis peut-être trompé.

Il y a plus d'un an, et je ne me souviens pas des circonstances exactes, mais je pense que j'ai probablement manqué le fait que les index uniques ont une exception partielle des règles de visibilité des transactions afin de permettre à une transaction d'insertion d'attendre qu'une autre s'insère ou roule avant de continuer.

Ou peut-être ai-je manqué le fait que INSERTdans le wCTE est conditionnel à la présence UPDATEde lignes affectées, et non pas à la présence ou non de la ligne candidate dans le tableau.

INSERTS en conflit sur un index unique attend la validation / restauration

Supposons qu'une copie de la requête s'exécute, en insérant une ligne. Le changement n'est pas encore engagé. Le nouveau tuple existe dans le tas et l'index unique, mais il n'est pas encore visible pour les autres transactions, quels que soient les niveaux d'isolement.

Maintenant, une autre copie de la requête s'exécute. La ligne insérée n'est pas encore visible car la première copie n'est pas validée, donc la mise à jour ne correspond à rien. La requête continuera pour tenter une insertion, qui verra qu'une autre transaction en cours insère cette même clé et bloquera l'attente de validation ou de restauration de cette transaction .

Si la première transaction est validée, la seconde échouera avec une violation unique, comme indiqué ci-dessus. Si la première transaction est annulée, la seconde procédera à la place à son insertion.

Le fait d' INSERTêtre dépendant du nombre de UPDATElignes protège contre les mises à jour perdues

Contrairement au cas à deux déclarations, je ne pense pas que le wCTE soit vulnérable aux mises à jour perdues.

Si le UPDATEn'a aucun effet, le INSERTsera toujours exécuté, car il est strictement conditionnel à ce qu'il ait UPDATEfait quelque chose, pas à l'état de la table externe. Il peut donc toujours échouer avec une violation unique, mais il ne peut pas manquer silencieusement d'avoir aucun effet et perdre complètement la mise à jour.

Craig Ringer
la source