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

Plusieurs threads peuvent-ils provoquer des mises à jour en double sur un ensemble contraint ?

Vos garanties énoncées s'appliquent dans ce cas simple, mais pas nécessairement dans des requêtes légèrement plus complexes. Voir la fin de la réponse pour des exemples.

Le cas simple

En supposant que col1 est unique, a exactement une valeur "2", ou a un ordre stable, donc chaque UPDATE correspond aux mêmes lignes dans le même ordre :

Ce qui se passera pour cette requête, c'est que les threads trouveront la ligne avec col=2 et essaieront tous de saisir un verrou en écriture sur ce tuple. Exactement l'un d'entre eux réussira. Les autres bloqueront l'attente de la validation de la transaction du premier thread.

Ce premier tx écrira, validera et renverra un nombre de lignes de 1. Le commit libérera le verrou.

Les autres tx essaieront à nouveau de saisir la serrure. Un par un, ils réussiront. Chaque transaction passera à son tour par le processus suivant :

  • Obtenez le verrou en écriture sur le tuple contesté.
  • Re-vérifier le WHERE col=2 condition après avoir obtenu le verrou.
  • La nouvelle vérification montrera que la condition ne correspond plus, donc le UPDATE sautera cette ligne.
  • La UPDATE n'a pas d'autres lignes, il signalera donc qu'aucune ligne n'a été mise à jour.
  • Commitez, en libérant le verrou pour le prochain tx essayant de le saisir.

Dans ce cas simple, le verrouillage au niveau de la ligne et la re-vérification de la condition sérialisent efficacement les mises à jour. Dans les cas plus complexes, pas tellement.

Vous pouvez facilement le démontrer. Ouvrez disons quatre sessions psql. Dans le premier, verrouillez la table avec BEGIN; LOCK TABLE test; . Dans le reste des sessions, exécutez la même UPDATE s - ils bloqueront le verrou au niveau de la table. Libérez maintenant le verrou par COMMIT lors de votre première session. Regardez-les courir. Un seul rapportera un nombre de lignes de 1, les autres rapporteront 0. Ceci est facilement automatisé et scripté pour la répétition et la mise à l'échelle vers plus de connexions/threads.

Pour en savoir plus, lisez les règles d'écriture simultanée , page 11 de Problèmes de concurrence PostgreSQL - puis lisez le reste de cette présentation.

Et si col1 n'est pas unique ?

Comme Kevin l'a noté dans les commentaires, si col n'est pas unique, vous pouvez donc faire correspondre plusieurs lignes, puis différentes exécutions de la UPDATE pourrait obtenir différentes commandes. Cela peut arriver s'ils choisissent des plans différents (disons que l'un est un via un PREPARE et EXECUTE et un autre est direct, ou vous jouez avec le enable_ GUC) ou si le plan qu'ils utilisent tous utilise une sorte instable de valeurs égales. S'ils obtiennent les lignes dans un ordre différent, alors tx1 verrouillera un tuple, tx2 en verrouillera un autre, puis ils essaieront chacun d'obtenir des verrous sur les tuples déjà verrouillés des autres. PostgreSQL abandonnera l'un d'entre eux avec une exception de blocage. C'est encore une autre bonne raison pour laquelle tous votre code de base de données doit toujours soyez prêt à réessayer les transactions.

Si vous faites attention à vous assurer que UPDATE simultané s obtiennent toujours les mêmes lignes dans le même ordre, vous pouvez toujours vous fier au comportement décrit dans la première partie de la réponse.

Frustrant, PostgreSQL n'offre pas UPDATE ... ORDER BY donc s'assurer que vos mises à jour sélectionnent toujours les mêmes lignes dans le même ordre n'est pas aussi simple que vous le souhaiteriez. A SELECT ... FOR UPDATE ... ORDER BY suivi d'un UPDATE séparé est souvent le plus sûr.

Requêtes plus complexes, systèmes de file d'attente

Si vous effectuez des requêtes avec plusieurs phases, impliquant plusieurs tuples ou des conditions autres que l'égalité, vous pouvez obtenir des résultats surprenants qui diffèrent des résultats d'une exécution en série. En particulier, les exécutions simultanées de quelque chose comme :

UPDATE test SET col = 1 WHERE col = (SELECT t.col FROM test t ORDER BY t.col LIMIT 1);

ou d'autres efforts pour construire un système de "file d'attente" simple sera *échoue* à fonctionner comme vous l'espérez. Voir la docs PostgreSQL sur la concurrence et cette présentation pour plus d'informations.

Si vous voulez une file d'attente de travail soutenue par une base de données, il existe des solutions éprouvées qui gèrent tous les cas particuliers étonnamment compliqués. L'un des plus populaires est PgQ . Il existe un article utile sur PgCon sur le sujet, et une recherche Google pour 'postgresql queue' regorge de résultats utiles.

BTW, au lieu d'un LOCK TABLE vous pouvez utiliser SELECT 1 FROM test WHERE col = 2 FOR UPDATE; pour obtenir un verrou en écriture uniquement sur ce tuple. Cela bloquera les mises à jour, mais ne bloquera pas les écritures sur d'autres tuples ni les lectures. Cela vous permet de simuler différents types de problèmes de concurrence.