Verrouillage dans Postgres pour la combinaison UPDATE / INSERT

11

J'ai deux tables. L'un est une table de journal; un autre contient essentiellement des codes de réduction qui ne peuvent être utilisés qu'une seule fois.

L'utilisateur doit pouvoir échanger un coupon, qui insérera une ligne dans la table de journal et marquera le coupon comme utilisé (en mettant à jour la usedcolonne vers true).

Naturellement, il y a un problème de condition de concurrence / sécurité évident ici.

J'ai fait des choses similaires dans le passé dans le monde de mySQL. Dans ce monde, je verrouillerais les deux tables globalement, ferais la logique en sachant que cela ne pourrait se produire qu'une fois à la fois, puis déverrouillerais les tables une fois que j'aurais terminé.

Y a-t-il une meilleure façon de faire cela à Postgres? En particulier, je crains que le verrou soit global, mais cela ne doit pas l'être - je n'ai vraiment besoin que de m'assurer que personne d'autre n'entre ce code particulier, alors peut-être qu'un verrouillage au niveau de la ligne fonctionnerait?

Rob Miller
la source

Réponses:

15

J'ai déjà entendu parler de problèmes de concurrence comme celui-ci dans MySQL. Ce n'est pas le cas à Postgres.

Les verrous intégrés au niveau des lignes dans le READ COMMITTEDniveau d'isolement des transactions par défaut sont suffisants.

Je suggère une seule déclaration avec un CTE de modification des données (quelque chose que MySQL n'a pas non plus) car il est pratique de passer directement les valeurs d'une table à l'autre (si vous en avez besoin). Si vous n'avez besoin de rien de la coupontable, vous pouvez également utiliser une transaction avec des instructions distinctes UPDATEet INSERT.

WITH upd AS (
   UPDATE coupon
   SET    used = true
   WHERE  coupon_id = 123
   AND    NOT used
   RETURNING coupon_id, other_column
   )
INSERT INTO log (coupon_id, other_column)
SELECT coupon_id, other_column FROM upd;

Il devrait être rare que plus d'une transaction essaie de racheter le même coupon. Ils ont un numéro unique, non? Plus d'une transaction essayant au même moment devrait être encore beaucoup plus rare. (Peut-être un bug d'application ou quelqu'un essayant de jouer au système?)

Quoi qu'il en soit, le UPDATEseul réussit pour exactement une transaction, quoi qu'il arrive . An UPDATEacquiert un verrou de niveau ligne sur chaque ligne cible avant la mise à jour. Si une transaction simultanée essaie UPDATEla même ligne, elle verra le verrou sur la ligne et attendra que la transaction de blocage soit terminée ( ROLLBACKou COMMIT), puis être la première dans la file d'attente de verrouillage:

  • Si commis, revérifiez la condition. Si c'est encore NOT used, verrouillez la rangée et continuez. Sinon, le UPDATEne trouve maintenant aucune ligne de qualification et ne fait rien , ne renvoyant aucune ligne, donc le INSERTne fait rien non plus.

  • S'il est reculé, verrouillez la rangée et continuez.

Il n'y a aucun potentiel de condition de course .

Il n'y a aucun risque de blocage à moins que vous ne mettiez plus d'écritures dans la même transaction ou que vous ne bloquiez autrement plus de lignes que la seule.

Le INSERTest sans souci. Si, par erreur, le coupon_iddéjà est dans la logtable (et que vous avez une contrainte UNIQUE ou PK activée log.coupon_id), la transaction entière sera annulée après une violation unique. Indiquerait un état illégal dans votre base de données. Si l'instruction ci-dessus est le seul moyen d'écrire dans la logtable, cela ne devrait jamais se produire.

Erwin Brandstetter
la source
Il devrait en effet être une chose rare que plus d'une transaction essaie de racheter le même code, mais vos soupçons ont raison en ce sens que cela se fera exclusivement lorsque quelqu'un essaiera de jouer au système. Merci beaucoup pour cela - les CTE ont été un grand attrait pour moi en passant à Postgres, mais je ne savais pas que le verrouillage implicite serait suffisant pour cela.
Rob Miller