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?
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.