J'ai une base de données Postgres qui contient des détails sur les clusters de serveurs, tels que le statut du serveur ('actif', 'en veille', etc.). Les serveurs actifs à tout moment peuvent avoir besoin de basculer en mode veille, et peu importe le type de veille utilisé.
Je souhaite qu'une requête de base de données modifie le statut d'un serveur en veille - JUST ONE - et renvoie l'adresse IP du serveur à utiliser. Le choix peut être arbitraire: puisque l'état du serveur change avec la requête, le mode de veille sélectionné n'a pas d'importance.
Est-il possible de limiter ma requête à une seule mise à jour?
Voici ce que j'ai jusqu'à présent:
UPDATE server_info SET status = 'active'
WHERE status = 'standby' [[LIMIT 1???]]
RETURNING server_ip;
Postgres n'aime pas ça. Que pourrais-je faire différemment?
postgresql
update
concurrency
queue
Vaste élève supérieur
la source
la source
Réponses:
Sans accès en écriture simultané
Matérialiser une sélection dans un CTE et la rejoindre dans la
FROM
clause de laUPDATE
.A l'origine, j'avais une sous-requête simple, mais cela peut éviter les
LIMIT
plans de requête, comme l' a souligné Feike :Ou utilisez une sous-requête faiblement corrélée pour le cas simple avec
LIMIT
1
. Plus simple, plus rapide:Avec accès en écriture simultané
En supposant un niveau d'isolation par défaut
READ COMMITTED
pour tout cela. Des niveaux d'isolation plus stricts (REPEATABLE READ
etSERIALIZABLE
) peuvent toujours entraîner des erreurs de sérialisation. Voir:Sous charge d'écriture simultanée, ajoutez cette
FOR UPDATE SKIP LOCKED
option pour verrouiller la ligne afin d'éviter les situations de concurrence .SKIP LOCKED
a été ajouté dans Postgres 9.5 , pour les anciennes versions, voir ci-dessous. Le manuel:S'il ne reste aucune ligne qualifiée, non verrouillée, rien ne se passe dans cette requête (aucune ligne n'est mise à jour) et vous obtenez un résultat vide. Pour les opérations non critiques, cela signifie que vous avez terminé.
Cependant, les transactions simultanées peuvent avoir des lignes verrouillées, mais ne terminez pas la mise à jour (
ROLLBACK
ou d'autres raisons). Pour être sûr de faire une dernière vérification:SELECT
voit également les lignes verrouillées. Si ce n'est pas le castrue
, une ou plusieurs lignes sont toujours en cours de traitement et les transactions peuvent toujours être annulées. (Ou de nouvelles lignes ont été ajoutées entre-temps.) Attendez un peu, puisUPDATE
passez en boucle les deux étapes: ( jusqu'à ce que vous n'ayez plus de ligne;SELECT
...) jusqu'à ce que vous obtenieztrue
.Apparenté, relié, connexe:
Sans
SKIP LOCKED
PostgreSQL 9.4 ou plus ancienLes transactions simultanées qui tentent de verrouiller la même ligne sont bloquées jusqu'à ce que la première libère son verrou.
Si la première a été annulée, la transaction suivante prend le verrou et se poursuit normalement; d'autres dans la file d'attente continuent d'attendre.
Si le premier est validé, la
WHERE
condition est réévaluée et si ce n'estTRUE
plus le cas (status
a changé), le CTE ne retourne pas (de manière assez surprenante) aucune ligne. Rien ne se passe. C'est le comportement souhaité lorsque toutes les transactions souhaitent mettre à jour la même ligne .Mais pas lorsque chaque transaction veut mettre à jour la ligne suivante . Et comme nous souhaitons simplement mettre à jour une ligne arbitraire (ou aléatoire ) , il est inutile d'attendre du tout.
Nous pouvons débloquer la situation à l'aide de verrous consultatifs :
De cette façon, la prochaine ligne non encore verrouillée sera mise à jour. Chaque transaction reçoit une nouvelle ligne avec laquelle travailler. J'ai eu l'aide de Czech Postgres Wiki pour cette astuce.
id
être n'importe quellebigint
colonne unique (ou n'importe quel type avec une distribution implicite telle queint4
ouint2
).Si des verrous de mise en garde sont utilisés simultanément pour plusieurs tables de votre base de données, désambiguïsez-vous
pg_try_advisory_xact_lock(tableoid::int, id)
-id
soyez un uniqueinteger
ici.Puisqu'il
tableoid
s'agit d'unebigint
quantité, il peut théoriquement déborderinteger
. Si vous êtes assez paranoïaque, utilisez(tableoid::bigint % 2147483648)::int
plutôt - laissant une "collision de hachage" théorique pour les véritablement paranoïaques ...En outre, Postgres est libre de tester les
WHERE
conditions dans n'importe quel ordre. Cela pourrait permettre de testerpg_try_advisory_xact_lock()
et d'acquérir un verrou avantstatus = 'standby'
, ce qui pourrait entraîner des verrous supplémentaires sur les lignes non liées, ce quistatus = 'standby'
n'est pas vrai. Question connexe sur SO:En règle générale, vous pouvez simplement ignorer cela. Pour garantir que seules les lignes qualifiantes sont verrouillées, vous pouvez imbriquer le ou les prédicats dans un CTE comme ci-dessus ou dans une sous-requête avec le
OFFSET 0
hack (empêche l'inlining) . Exemple:Ou (moins cher pour les analyses séquentielles) imbriquer les conditions dans une
CASE
déclaration telle que:Cependant, l’
CASE
astuce empêcherait également Postgres d’utiliser un indexstatus
. Si un tel index est disponible, vous n'avez pas besoin d'une imbrication supplémentaire pour commencer: seules les lignes qualifiantes seront verrouillées dans une analyse d'index.Comme vous ne pouvez pas être sûr qu'un index est utilisé dans chaque appel, vous pouvez simplement:
Le
CASE
est logiquement redondant, mais il sert le but discuté.Si la commande fait partie d'une transaction longue, envisagez des verrous au niveau de la session qui peuvent être (et doivent être) libérés manuellement. Vous pouvez donc déverrouiller dès que vous avez terminé avec la ligne verrouillée:
pg_try_advisory_lock()
etpg_advisory_unlock()
. Le manuel:Apparenté, relié, connexe:
la source