Je construis une sorte de mécanisme de mise en file d'attente. Il existe des lignes de données qui doivent être traitées et un indicateur d'état. J'utilise un update .. returning
clause pour le gérer:
UPDATE stuff
SET computed = 'working'
WHERE id = (SELECT id from STUFF WHERE computed IS NULL LIMIT 1)
RETURNING *
La partie sélectionnée imbriquée est-elle le même verrou que la mise à jour, ou ai-je ici une condition de concurrence critique? Si tel est le cas, la sélection interne doit-elle être un select for update
?
Alors que la suggestion d'Erwin est probablement la manière la plus simple d'obtenir un comportement correct (tant que vous réessayez votre transaction si vous obtenez une exception avec SQLSTATE
de 40001), les applications de mise en file d'attente de par leur nature ont tendance à mieux fonctionner avec le blocage des demandes pour avoir une chance de prendre leur tour dans la file d'attente qu'avec l'implémentation PostgreSQL des transactions SERIALIZABLE
, ce qui permet une concurrence plus élevée et est un peu plus "optimiste" "sur les chances de collision.
L'exemple de requête dans la question, en l'état, dans la valeur par défaut READ COMMITTED
le niveau d'isolement des transactions permettrait à deux (ou plus) connexions simultanées de "revendiquer" la même ligne de la file d'attente. Ce qui va arriver, c'est ceci:
UPDATE
.COMMIT
ou ROLLBACK
de T1.id
correspond), et "revendique" également la rangée.Il peut être modifié pour fonctionner correctement (si vous utilisez une version de PostgreSQL qui autorise le FOR UPDATE
clause dans une sous-requête). Il suffit d'ajouter FOR UPDATE
à la fin de la sous-requête qui sélectionne l'identifiant, et cela se produira:
COMMIT
ou ROLLBACK
de T1.Au REPEATABLE READ
ou SERIALIZABLE
au niveau d'isolement des transactions, le conflit d'écriture génèrerait une erreur, que vous pourriez détecter et déterminer s'il s'agissait d'un échec de sérialisation basé sur SQLSTATE, puis réessayer.
Si vous souhaitez généralement des transactions SERIALISABLES mais que vous souhaitez éviter les tentatives dans la zone de mise en file d'attente, vous pouvez peut-être y parvenir en utilisant un verrouillage consultatif .
Si vous êtes le seul utilisateur , la requête devrait être correcte. En particulier, il n'y a ni condition de concurrence ni blocage dans la requête elle-même (entre la requête externe et la sous-requête). Je cite le manuel ici :
Cependant, une transaction n'est jamais en conflit avec elle-même.
Pour utilisation simultanée , la question peut être plus compliquée. Vous seriez du bon côté avec SERIALIZABLE
mode de transaction :
BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE stuff
SET computed = 'working'
WHERE id = (SELECT id FROM stuff WHERE computed IS NULL LIMIT 1)
RETURNING *
COMMIT;
Vous devez vous préparer aux échecs de sérialisation et réessayer votre requête dans un tel cas.
Mais je ne suis pas entièrement sûr que ce ne soit pas exagéré. Je vais demander à @kgrittn de s'arrêter .. il est le expert des transactions simultanées et sérialisables ..
Exécutez la requête en mode de transaction par défaut READ COMMITTED
.
Pour Postgres 9.5 ou version ultérieure, utilisez FOR UPDATE SKIP LOCKED
. Voir:
Pour les versions plus anciennes, revérifiez la condition computed IS NULL
explicitement dans le UPDATE
externe:
UPDATE stuff
SET computed = 'working'
WHERE id = (SELECT id FROM stuff WHERE computed IS NULL LIMIT 1)
AND computed IS NULL;
Comme @ kgrittn l'a indiqué dans le commentaire de sa réponse, cette requête peut apparaître vide, sans avoir rien fait, dans le cas (peu probable) où elle est liée à une transaction simultanée.
Par conséquent, cela fonctionnerait un peu comme la première variante en mode de transaction SERIALIZABLE
, vous devrez réessayer - juste sans la pénalité de performance.
Le seul problème: bien que le conflit soit très improbable car la fenêtre d'opportunité est si petite, il peut se produire sous une lourde charge. Vous ne pouviez pas dire avec certitude s'il ne restait finalement plus de lignes.
Si cela n'a pas d'importance (comme dans votre cas), vous avez terminé ici.
Si c'est le cas, pour être absolument sûr, lancez une autre requête avec verrouillage explicite après avoir obtenu un résultat vide. Si cela apparaît vide, vous avez terminé. Sinon, continuez.
Dans plpgsql cela pourrait ressembler à ceci:
LOOP
UPDATE stuff
SET computed = 'working'
WHERE id = (SELECT id FROM stuff WHERE computed IS NULL
LIMIT 1 FOR UPDATE SKIP LOCKED); -- pg 9.5+
-- WHERE id = (SELECT id FROM stuff WHERE computed IS NULL LIMIT 1)
-- AND computed IS NULL; -- pg 9.4-
CONTINUE WHEN FOUND; -- continue outside loop, may be a nested loop
UPDATE stuff
SET computed = 'working'
WHERE id = (SELECT id FROM stuff WHERE computed IS NULL
LIMIT 1 FOR UPDATE);
EXIT WHEN NOT FOUND; -- exit function (end)
END LOOP;
Cela devrait vous donner le meilleur des deux mondes: performances et fiabilité.