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


35

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

CREATE TABLE test (id INT PRIMARY KEY);

et quelques rangées:

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ù réside cette table. Les deux sont transactionnels (c.-à-d. Auto-commit = false). Appelons-les S1 et S2.

Le même bit de code pour chacun:

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

Maintenant, exécutez ceci 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 très bien dans SQLServer. Lorsque S2 supprime, il signale 1 ligne supprimée. Et puis l'insert de S2 fonctionne bien.

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

Ai-je raison? Cela peut-il fonctionner?

Réponses:


39

Mat et Erwin ont raison, et j'ajoute une autre réponse pour développer davantage ce qu'ils ont dit d'une manière qui ne rentre pas dans un commentaire. Comme 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 vais élaborer.

Le point important ici est qu’en vertu de la norme SQL, dans une transaction exécutée au READ COMMITTEDniveau d’isolation de transaction, la restriction est que le travail des transactions non validées ne doit pas être visible. Lorsque le travail des transactions validées devient visible dépend de la mise en œuvre. Ce que vous soulignez est une différence dans la façon dont deux produits ont choisi de mettre en œuvre cela. Aucune de ces implémentations ne viole les exigences de la norme.

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

S1-1 fonctionne (1 ligne supprimée)

L'ancienne ligne est laissée en place, car S1 peut toujours revenir en arrière, mais S1 maintient maintenant un verrou sur la ligne afin que toute autre session tentant de modifier la ligne attende de voir si S1 est validée ou annulée. Toute lecture de la table peut toujours voir l'ancienne ligne, à moins qu'ils ne tentent de la verrouiller avec SELECT FOR UPDATEou SELECT FOR SHARE.

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

S2 doit maintenant attendre le résultat de S1. Si S1 devait revenir en arrière plutôt que valider, S2 supprimerait la ligne. Notez que si S1 insérait 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 pistes (1 ligne insérée)

Cette rangée est indépendante de l'ancienne. S'il y avait eu une mise à jour de la ligne avec id = 1, l'ancienne version et la nouvelle version 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 existante dans le passé ne la rend pas identique à une version mise à jour de cette ligne.

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

Donc, les modifications de S1 sont persistées. Une rangée est partie. 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 y a un pointeur d'une version d'une ligne à la version suivante 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'une READ COMMITTEDtransaction sort d'un blocage sur un conflit d'écriture, elle 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.

S2 peut ou non accéder à la nouvelle ligne lors de son balayage de la table. Si tel est le cas, il verra que la nouvelle ligne a été créée après le lancement de l' DELETEinstruction S2 et ne fait donc pas partie de l'ensemble des lignes visibles.

Si PostgreSQL devait redémarrer l'intégralité de l'instruction DELETE de S2 depuis le début avec un nouvel instantané, celle-ci 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 remarqueriez jamais la différence de performances, mais si vous aviez dix millions de lignes dans une situation de DELETEblocage, vous le feriez certainement. Dans ce cas, PostgreSQL a choisi la performance, car la version la 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 la photo.

Bien qu'il y ait un comportement surprenant ici, tout est conforme au standard SQL et à la limite 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 essaye très difficilement d'éviter les échecs de sérialisation dans le READ COMMITTEDniveau d'isolement et autorise certains comportements qui diffèrent des autres produits pour y parvenir.

Personnellement, je ne suis pas un grand fan du READ COMMITTEDniveau d’isolation des transactions dans l’implémentation d’ un produit. Ils permettent tous à des conditions de concurrence de créer des comportements surprenants d’un point de vue transactionnel. Une fois que quelqu'un s'habitue aux comportements étranges permis par un produit, ils ont tendance à considérer cela comme "normal" et les compromis choisis par un autre produit bizarre. Mais chaque produit doit faire un compromis quelconque pour tout mode non implémenté SERIALIZABLE. Les développeurs de PostgreSQL ont choisi de tracer la ligne READ COMMITTEDpour minimiser le blocage (les lectures ne bloquent pas les écritures et les écritures ne bloquent pas les lectures) et minimisent les risques d'échec de la sérialisation.

La norme exige que les SERIALIZABLEtransactions soient la transaction par défaut, mais la plupart des produits ne le font pas, car cela nuit aux performances des niveaux d'isolation des transactions plus laxistes. Certains produits n'offrent même pas de transactions véritablement sérialisables lors de la SERIALIZABLEsélection, notamment Oracle et les versions de PostgreSQL antérieures à la 9.1. Cependant, utiliser véritablement des SERIALIZABLEtransactions est le seul moyen d’éviter des effets surprenants liés aux conditions de concurrence, et les SERIALIZABLEtransactions doivent toujours soit bloquer pour éviter les conditions de concurrence, soit annuler certaines transactions pour éviter une situation de concurrence en développement. L'implémentation la plus courante des SERIALIZABLEtransactions est le verrouillage strict en deux phases (S2PL), qui présente des défaillances de blocage et de sérialisation (sous la forme d'interblocages).

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


Je me demande si un moyen vraiment pas cher (cheesy?) De faire ce travail est d’émettre deux DELETES suivies de l’INSERT. Dans mes tests limités (2 threads), cela a bien fonctionné, mais il faut en tester davantage pour voir si cela serait valable pour de nombreux threads.
DaveyBob

Tant que vous utilisez des READ COMMITTEDtransactions, vous avez une condition de concurrence critique: que se passerait-il si une autre transaction insérait une nouvelle ligne après la première DELETEet avant la seconde DELETE? Avec des transactions moins strictes que SERIALIZABLEles deux manières principales de clore une course, il faut promouvoir un conflit (mais cela ne sert à rien lorsque la ligne est supprimée) et la matérialisation d'un conflit. Vous pouvez matérialiser le conflit en ayant une table "id" mise à jour pour chaque ligne supprimée ou en la verrouillant explicitement. Ou utilisez des tentatives en cas d'erreur.
Kgrittn

Il essaie de nouveau. Merci beaucoup pour cette précieuse idée!
DaveyBob

21

Je crois que cela est inhérent à la conception, selon la description du niveau d'isolement en lecture seule de 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 rechercheront que les lignes cibles validées à partir de l'heure de début de la commande 1 . Cependant, une telle ligne cible a peut-être 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 procéder à la mise à jour de la ligne trouvée à l'origine. Si le premier programme de mise à jour est validé, le second programme de mise à jour ignorera la ligne si le premier programme de mise à jour l'a supprimée 2sinon, il tentera d'appliquer son opération à la version mise à jour de la ligne.

La ligne que vous insérez dans S1n'existait pas encore quand S2est DELETEcommencé. Donc, il ne sera pas vu par la suppression dans S2( 1 ) ci-dessus. Celui qui a été S1supprimé est ignoré par S2s DELETEselon ( 2 ).

Donc, dans S2la suppression ne fait rien. Lorsque l'insert arrive cependant, que l' on ne fait voir S1l'insert de »:

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

La tentative d'insertion par S2échoue donc avec la violation de contrainte.

Continuer à lire ce document, utiliser une lecture répétée 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 cependant de réessayer la transaction.


Merci Mat. Bien que cela semble être ce qui se passe, il semble y avoir une faille dans cette logique. Il me semble que, dans un niveau iso READ_COMMITTED, ces deux instructions doivent réussir à l'intérieur d'un tx: DELETE FROM test WHERE ID = 1 INSERT INTO VALUES (1) Je veux dire, si je supprime la ligne, puis l'insère, alors cet insert devrait réussir. SQLServer obtient ce droit. Dans l’état actuel des choses, j’ai beaucoup de difficulté à gérer cette situation dans un produit devant fonctionner avec les deux bases de données.
DaveyBob

11

Je suis tout à fait d'accord avec l'excellente réponse de @ Mat . Je n'écris qu'une autre réponse, car cela ne rentrerait pas dans un commentaire.

En réponse à votre commentaire: Le DELETEdans S2 est déjà accroché à une version de ligne particulière. Puisque ceci est tué entre-temps par S1, S2 se considère comme ayant réussi. Bien que cela ne soit pas évident d’un coup d’œil rapide, la série d’événements est pratiquement la suivante:

   S1 DELETE réussi  
S2 DELETE (avec succès par procuration - DELETE à partir de S1)  
   S1 réinsère la valeur supprimée virtuellement dans l'intervalle  
S2 INSERT échoue avec une violation de contrainte de clé unique

C'est tout par conception. Vous devez vraiment utiliser des SERIALIZABLEtransactions pour vos besoins et vous assurer de réessayer en cas d’échec de la sérialisation.



-2

Nous avons également fait face à ce problème. Notre solution ajoute select ... for updateavant delete from ... where. Le niveau d'isolement doit être lu, engagé.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.