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 COMMITTED
niveau 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 UPDATE
ou 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 COMMITTED
transaction 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' DELETE
instruction 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 DELETE
blocage, 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 COMMITTED
niveau 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 COMMITTED
niveau 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 COMMITTED
pour 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 SERIALIZABLE
transactions 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 SERIALIZABLE
sélection, notamment Oracle et les versions de PostgreSQL antérieures à la 9.1. Cependant, utiliser véritablement des SERIALIZABLE
transactions est le seul moyen d’éviter des effets surprenants liés aux conditions de concurrence, et les SERIALIZABLE
transactions 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 SERIALIZABLE
transactions 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.
READ COMMITTED
transactions, vous avez une condition de concurrence critique: que se passerait-il si une autre transaction insérait une nouvelle ligne après la premièreDELETE
et avant la secondeDELETE
? Avec des transactions moins strictes queSERIALIZABLE
les 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.Je crois que cela est inhérent à la conception, selon la description du niveau d'isolement en lecture seule de PostgreSQL 9.2:
La ligne que vous insérez dans
S1
n'existait pas encore quandS2
estDELETE
commencé. Donc, il ne sera pas vu par la suppression dansS2
( 1 ) ci-dessus. Celui qui a étéS1
supprimé est ignoré parS2
sDELETE
selon ( 2 ).Donc, dans
S2
la suppression ne fait rien. Lorsque l'insert arrive cependant, que l' on ne fait voirS1
l'insert de »: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.
la source
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
DELETE
dans 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:C'est tout par conception. Vous devez vraiment utiliser des
SERIALIZABLE
transactions pour vos besoins et vous assurer de réessayer en cas d’échec de la sérialisation.la source
Utilisez une clé primaire DEFERRABLE et réessayez.
la source
Nous avons également fait face à ce problème. Notre solution ajoute
select ... for update
avantdelete from ... where
. Le niveau d'isolement doit être lu, engagé.la source