web-dev-qa-db-fra.com

Verrouillage dans Postgres pour la combinaison UPDATE / INSERT

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 des journaux et marquera le coupon comme utilisé (en mettant à jour la colonne used en true).

Naturellement, il y a un problème évident de condition de concurrence/sécurité 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?

11
Rob Miller

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

Verrous intégrés au niveau de la ligne par défaut READ COMMITTED niveau d'isolement des transactions suffisent.

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 des valeurs d'une table à l'autre (si vous en avez besoin). Si vous n'avez besoin de rien de la table coupon, vous pouvez également utiliser une transaction avec des instructions UPDATE et INSERT distinctes.

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;

Cela devrait être une chose 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 beaucoup plus rare encore. (Peut-être un bug d'application ou quelqu'un essayant de jouer au système?)

Quoi qu'il en soit, la transaction UPDATE ne réussit que pour exactement une transaction, quoi qu'il arrive . Un UPDATE acquiert un verrou de niveau de ligne sur chaque ligne cible avant la mise à jour. Si une transaction simultanée essaie de UPDATE la même ligne, elle verra le verrou sur la ligne et attendra que la transaction de blocage soit terminée (ROLLBACK ou COMMIT), étant alors le premier dans la file d'attente de verrouillage:

  • Si commis, revérifiez la condition. Si c'est toujours NOT used, verrouillez la ligne et continuez. Sinon, UPDATE ne trouve plus de ligne de qualification et ne rien, ne renvoyant aucune ligne, donc le INSERT ne fait rien non plus.

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

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

Il n'y a aucun potentiel pour un blocage sauf si vous mettez plus d'écritures dans la même transaction ou verrouillez plus de lignes que celle-ci. .

Le INSERT est sans souci. Si, par erreur, le coupon_id est déjà dans la table log (et vous avez une contrainte UNIQUE ou PK sur log.coupon_id), toute la transaction 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 table log, cela ne devrait jamais se produire.

15
Erwin Brandstetter