web-dev-qa-db-fra.com

Problème de verrouillage avec DELETE / INSERT simultané dans PostgreSQL

C'est assez simple, mais je suis déconcerté par ce que fait PG (v9.0). Nous commençons par un simple tableau:

CREATE TABLE test (id INT PRIMARY KEY);

et quelques lignes:

INSERT INTO TEST VALUES (1);
INSERT INTO TEST VALUES (2);

À l'aide de mon outil de requête JDBC préféré (ExecuteQuery), je connecte deux fenêtres de session à la base de données où se trouve cette table. Les deux sont transactionnels (c'est-à-dire, auto-commit = false). Appelons-les S1 et S2.

Le même morceau de code pour chacun:

1:DELETE FROM test WHERE id=1;
2:INSERT INTO test VALUES (1);
3:COMMIT;

Maintenant, exécutez cela au ralenti, en exécutant un à la fois dans les fenêtres.

S1-1 runs (1 row deleted)
S2-1 runs (but is blocked since S1 has a write lock)
S1-2 runs (1 row inserted)
S1-3 runs, releasing the write lock
S2-1 runs, now that it can get the lock. But reports 0 rows deleted. HUH???
S2-2 runs, reports a unique key constraint violation

Maintenant, cela fonctionne bien dans SQLServer. Lorsque S2 effectue la suppression, il signale 1 ligne supprimée. Et puis l'insert de S2 fonctionne très bien.

Je soupçonne que PostgreSQL verrouille l'index dans la table où cette ligne existe, tandis que SQLServer verrouille la valeur de clé réelle.

Ai-je raison? Est-ce que cela peut fonctionner?

36
DaveyBob

Mat et Erwin ont tous deux raison, et j'ajoute seulement une autre réponse pour développer davantage ce qu'ils ont dit d'une manière qui ne rentre pas dans un commentaire. Étant donné que leurs réponses ne semblent pas satisfaire tout le monde, et il a été suggéré que les développeurs de PostgreSQL soient consultés, et j'en suis un, je développerai.

Le point important ici est que sous la norme SQL, dans une transaction exécutée à READ COMMITTED niveau d'isolement des transactions, la restriction est que le travail des transactions non validées ne doit pas être visible. Quand le travail des transactions validées devient visible dépend de l'implémentation. Ce que vous signalez, c'est une différence dans la façon dont deux produits ont choisi de mettre en œuvre cela. Aucune implémentation ne viole les exigences de la norme.

Voici ce qui se passe dans PostgreSQL, en détail:

 S1-1 s'exécute (1 ligne supprimée) 

L'ancienne ligne est laissée en place, car S1 peut toujours revenir en arrière, mais S1 détient maintenant un verrou sur la ligne de sorte que toute autre session tentant de modifier la ligne attendra pour voir si S1 valide ou annule. N'importe quel lit du tableau peut toujours voir l'ancienne ligne, à moins qu'il ne tente de le verrouiller avec SELECT FOR UPDATE ou SELECT FOR SHARE.

 S2-1 s'exécute (mais est bloqué car S1 a un verrou en écriture) 

S2 doit maintenant attendre pour voir le résultat de S1. Si S1 devait annuler plutôt que valider, S2 supprimerait la ligne. Notez que si S1 a inséré une nouvelle version avant de revenir en arrière, la nouvelle version n'aurait jamais été là du point de vue d'une autre transaction, et l'ancienne version n'aurait pas été supprimée du point de vue d'une autre transaction.

 S1-2 s'exécute (1 ligne insérée) 

Cette ligne est indépendante de l'ancienne. S'il y avait eu une mise à jour de la ligne avec id = 1, les anciennes et les nouvelles versions seraient liées et S2 pourrait supprimer la version mise à jour de la ligne lorsqu'elle serait débloquée. Le fait qu'une nouvelle ligne ait les mêmes valeurs qu'une ligne qui existait dans le passé ne la rend pas identique à une version mise à jour de cette ligne.

 S1-3 s'exécute, libérant le verrouillage en écriture 

Les changements de S1 sont donc persistants. Une rangée a disparu. Une ligne a été ajoutée.

 S2-1 s'exécute, maintenant qu'il peut obtenir le verrou. Mais rapporte 0 lignes supprimées. HUH ??? 

Ce qui se passe en interne, c'est qu'il existe un pointeur d'une version d'une ligne vers la prochaine version de cette même ligne si elle est mise à jour. Si la ligne est supprimée, il n'y a pas de version suivante. Lorsqu'un READ COMMITTED transaction s'éveille d'un bloc sur un conflit d'écriture, il suit cette chaîne de mise à jour jusqu'à la fin; si la ligne n'a pas été supprimée et si elle répond toujours aux critères de sélection de la requête, elle sera traitée. Cette ligne a été supprimée, la requête de S2 continue donc.

S2 peut ou non accéder à la nouvelle ligne pendant son analyse de la table. Si c'est le cas, il verra que la nouvelle ligne a été créée après le début de l'instruction DELETE de S2, et ne fait donc pas partie de l'ensemble de lignes qui lui est visible.

Si PostgreSQL devait redémarrer l'intégralité de l'instruction DELETE de S2 depuis le début avec un nouvel instantané, il se comporterait de la même manière que SQL Server. La communauté PostgreSQL n'a pas choisi de le faire pour des raisons de performances. Dans ce cas simple, vous ne remarquerez jamais la différence de performances, mais si vous étiez dix millions de lignes dans un DELETE lorsque vous êtes bloqué, vous le feriez certainement. Il y a un compromis ici où PostgreSQL a choisi les performances, car la version plus rapide est toujours conforme aux exigences de la norme.

 S2-2 s'exécute, signale une violation de contrainte de clé unique 

Bien sûr, la ligne existe déjà. C'est la partie la moins surprenante de l'image.

Bien qu'il y ait un comportement surprenant ici, tout est conforme au standard SQL et dans les limites de ce qui est "spécifique à l'implémentation" selon le standard. Cela peut certainement être surprenant si vous supposez que le comportement d'une autre implémentation sera présent dans toutes les implémentations, mais PostgreSQL essaie très fort d'éviter les échecs de sérialisation dans le READ COMMITTED niveau d'isolement, et autorise certains comportements qui diffèrent des autres produits pour y parvenir.

Maintenant, personnellement, je ne suis pas un grand fan de la READ COMMITTED niveau d'isolation des transactions dans tout implémentation du produit. Ils permettent tous aux conditions de concurrence de créer des comportements surprenants d'un point de vue transactionnel. Une fois que quelqu'un s'est habitué aux comportements étranges autorisés par un produit, il a tendance à considérer que c'est "normal" et les compromis choisis par un autre produit sont étranges. Mais chaque produit doit faire une sorte de compromis pour tout mode qui n'est pas réellement implémenté comme SERIALIZABLE. Où les développeurs PostgreSQL ont choisi de tracer la ligne dans READ COMMITTED vise à minimiser le blocage (les lectures ne bloquent pas les écritures et les écritures ne bloquent pas les lectures) et à minimiser les risques d'échecs de sérialisation.

La norme requiert que SERIALIZABLE transactions soient la valeur par défaut, mais la plupart des produits ne le font pas car cela entraîne une baisse des performances sur les niveaux d'isolement des transactions les plus laxistes. Certains produits ne fournissent même pas de transactions réellement sérialisables lorsque SERIALIZABLE est choisi - notamment Oracle et les versions de PostgreSQL antérieures à 9.1. Mais l'utilisation de transactions vraiment SERIALIZABLE est le seul moyen d'éviter les effets surprenants des conditions de concurrence, et les transactions SERIALIZABLE doivent toujours bloquer pour éviter les conditions de concurrence ou annuler certaines transactions pour éviter une situation de concurrence en développement . L'implémentation la plus courante des transactions SERIALIZABLE est le verrouillage strict à deux phases (S2PL) qui présente des échecs de blocage et de sérialisation (sous la forme de blocages).

Divulgation complète: J'ai travaillé avec Dan Ports de MIT pour ajouter des transactions vraiment sérialisables à PostgreSQL version 9.1 en utilisant une nouvelle technique appelée Serializable Snapshot Isolation.

40
kgrittn

Je crois que c'est par conception, selon la description du niveau d'isolement validé en lecture pour PostgreSQL 9.2:

Les commandes UPDATE, DELETE, SELECT FOR UPDATE et SELECT FOR SHARE se comportent de la même manière que SELECT en termes de recherche de lignes cibles: elles ne trouveront que les lignes cibles qui ont été validées à l'heure de début de la commande 1. Cependant, une telle ligne cible peut avoir déjà été mise à jour (ou supprimée ou verrouillée) par une autre transaction simultanée au moment où elle est trouvée. Dans ce cas, le programme de mise à jour potentiel attendra que la première transaction de mise à jour soit validée ou annulée (si elle est toujours en cours). Si le premier programme de mise à jour est annulé, ses effets sont annulés et le deuxième programme de mise à jour peut poursuivre la mise à jour de la ligne trouvée à l'origine. Si le premier programme de mise à jour est validé, le deuxième programme de mise à jour ignorera la ligne si le premier programme de mise à jour l'a supprimée 2, sinon il tentera d'appliquer son opération à la version mise à jour de la ligne.

La ligne que vous insérez dans S1 N'existait pas encore lorsque S2DELETE a commencé. Donc, il ne sera pas vu par la suppression dans S2 Selon (1) au dessus de. Celui que S1 A supprimé est ignoré par le DELETE de S2 Selon (2).

Donc dans S2, La suppression ne fait rien. Lorsque l'insert arrive, celui-ci le fait voir l'encart de S1:

Étant donné que le mode de lecture validée démarre chaque commande avec un nouvel instantané qui inclut toutes les transactions validées jusqu'à cet instant, les commandes suivantes dans la même transaction verra les effets de la transaction simultanée validée dans tous les cas . Le point en litige ci-dessus est de savoir si une seule commande voit ou non une vue absolument cohérente de la base de données.

Ainsi, la tentative d'insertion par S2 Échoue avec la violation de contrainte.

Continuer à lire ce document, en utilisant lecture répétable ou même sérialisable ne résoudrait pas complètement votre problème - la deuxième session échouerait avec une erreur de sérialisation lors de la suppression.

Cela vous permettrait de réessayer la transaction.

21
Mat

Je suis entièrement d'accord avec l'excellente réponse de @Mat . J'écris seulement une autre réponse, car elle ne rentrerait pas dans un commentaire.

En réponse à votre commentaire: le DELETE dans S2 est déjà accroché à une version de ligne particulière. Comme cela est tué par S1 entre-temps, S2 se considère comme un succès. Bien qu'elle ne soit pas évidente d'un coup d'œil, la série d'événements est pratiquement la suivante:

 S1 DELETE réussi 
 S2 DELETE (réussi par proxy - DELETE de S1) 
 S1 ré-INSÉRER la valeur supprimée pratiquement en attendant  
 S2 INSERT échoue avec une violation de contrainte de clé unique 

Tout est de conception. Vous devez vraiment utiliser les transactions SERIALIZABLE pour vos besoins et assurez-vous de réessayer en cas d'échec de la sérialisation.

11
Erwin Brandstetter

Utilisez une clé primaire DÉFÉRABLE et réessayez.

0
Frank Heikens