Postgres AGGIORNAMENTO ... LIMIT 1


78

Ho un database Postgres che contiene dettagli sui cluster di server, come lo stato del server ("attivo", "standby" ecc.). I server attivi in ​​qualsiasi momento potrebbero dover eseguire il failover su uno standby e non mi interessa quale standby viene utilizzato in particolare.

Voglio che una query del database cambi lo stato di standby - SOLO UNO - e restituisca l'IP del server che deve essere usato. La scelta può essere arbitraria: poiché lo stato del server cambia con la query, non importa quale standby sia selezionato.

È possibile limitare la mia query a un solo aggiornamento?

Ecco quello che ho finora:

UPDATE server_info SET status = 'active' 
WHERE status = 'standby' [[LIMIT 1???]] 
RETURNING server_ip;

A Postgres non piace questo. Cosa potrei fare diversamente?


Basta selezionare il server nel codice e aggiungerlo come vincolato. Ciò consente anche di verificare prima le condizioni aggiuntive (più vecchie, più recenti, più recenti vive, meno caricate, stesso CC, rack diverso, minimi errori). La maggior parte dei protocolli di failover richiede comunque una qualche forma di determinismo.
Devo dire che il

@eckes Questa è un'idea interessante. Nel mio caso "scegliere il server nel codice" avrebbe significato prima leggere un elenco di server disponibili dal db e quindi aggiornare un record. Poiché molte istanze dell'applicazione potrebbero eseguire questa azione, esiste una condizione di competizione ed è necessaria un'operazione atomica (o era 5 anni fa). La scelta non doveva essere deterministica.
vastlysuperiorman

Risposte:


126

Senza accesso simultaneo alla scrittura

Materializza una selezione in un CTE e unisciti ad essa nella FROMclausola del UPDATE.

WITH cte AS (
   SELECT server_ip          -- pk column or any (set of) unique column(s)
   FROM   server_info
   WHERE  status = 'standby'
   LIMIT  1                  -- arbitrary pick (cheapest)
   )
UPDATE server_info s
SET    status = 'active' 
FROM   cte
WHERE  s.server_ip = cte.server_ip
RETURNING server_ip;

Inizialmente avevo una semplice subquery qui, ma questo può eludere il LIMITper determinati piani di query come ha sottolineato Feike :

Il pianificatore può scegliere di generare un piano che esegue un ciclo nidificato sulla LIMITingsottoquery, causando più UPDATEsdi LIMIT, ad esempio:

 Update on buganalysis [...] rows=5
   ->  Nested Loop
         ->  Seq Scan on buganalysis
         ->  Subquery Scan on sub [...] loops=11
               ->  Limit [...] rows=2
                     ->  LockRows
                           ->  Sort
                                 ->  Seq Scan on buganalysis

Riproduzione del test case

Il modo per risolvere quanto sopra era avvolgere la LIMITsubquery nel proprio CTE, poiché il CTE è materializzato non restituirà risultati diversi su diverse iterazioni del loop nidificato.

Oppure usa una subquery poco correlata per il caso semplice conLIMIT 1. Più semplice, più veloce:

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         )
RETURNING server_ip;

Con accesso in scrittura simultanea

Supponendo il livello di isolamento predefinitoREAD COMMITTED per tutto questo. Livelli di isolamento più severi ( REPEATABLE READe SERIALIZABLE) possono comunque causare errori di serializzazione. Vedere:

Con carico di scrittura simultaneo, aggiungi FOR UPDATE SKIP LOCKEDper bloccare la riga per evitare le condizioni di gara. SKIP LOCKEDè stato aggiunto in Postgres 9.5 , per le versioni precedenti vedi sotto. Il manuale:

Con SKIP LOCKED, tutte le righe selezionate che non possono essere immediatamente bloccate vengono ignorate. Saltare le righe bloccate offre una visione incoerente dei dati, quindi non è adatto per lavori di carattere generale, ma può essere utilizzato per evitare contese di blocco con più utenti che accedono a una tabella simile a una coda.

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         FOR    UPDATE SKIP LOCKED
         )
RETURNING server_ip;

Se non è disponibile alcuna riga qualificante, sbloccata, non accade nulla in questa query (nessuna riga viene aggiornata) e si ottiene un risultato vuoto. Per operazioni non critiche ciò significa che hai finito.

Tuttavia, le transazioni simultanee potrebbero avere righe bloccate, ma non completare l'aggiornamento ( ROLLBACKo altri motivi). Per essere sicuro esegui un controllo finale:

SELECT NOT EXISTS (
   SELECT 1
   FROM   server_info
   WHERE  status = 'standby'
   );

SELECTvede anche le file bloccate. Se non viene restituito true, una o più righe sono ancora in fase di elaborazione e le transazioni potrebbero comunque essere ripristinate. (O nel frattempo sono state aggiunte nuove righe.) Aspetta un po ', quindi esegui i due passaggi in sequenza: ( UPDATEfinché non ottieni nessuna riga indietro; SELECT...) finché non ottieni true.

Relazionato:

Senza SKIP LOCKEDPostgreSQL 9.4 o precedente

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         FOR    UPDATE
         )
RETURNING server_ip;

Le transazioni simultanee che tentano di bloccare la stessa riga vengono bloccate fino a quando la prima non rilascia il blocco.

Se il primo è stato ripristinato, la transazione successiva prende il blocco e procede normalmente; altri in coda continuano ad aspettare.

Se il primo commit, la WHEREcondizione viene rivalutata e se non è TRUEpiù ( statusè cambiata) il CTE (in qualche modo sorprendentemente) non restituisce alcuna riga. Non succede nulla. Questo è il comportamento desiderato quando tutte le transazioni vogliono aggiornare la stessa riga .
Ma non quando ogni transazione vuole aggiornare la riga successiva . E poiché vogliamo solo aggiornare una riga arbitraria (o casuale ) , non ha senso aspettare affatto.

Possiamo sbloccare la situazione con l'aiuto di blocchi consultivi :

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         AND    pg_try_advisory_xact_lock(id)
         LIMIT  1
         FOR    UPDATE
         )
RETURNING server_ip;

In questo modo, la riga successiva non ancora bloccata verrà aggiornata. Ogni transazione ottiene una nuova riga con cui lavorare. Ho ricevuto aiuto dal ceco Postgres Wiki per questo trucco.

idessere una bigintcolonna univoca (o qualsiasi tipo con un cast implicito come int4o int2).

Se i blocchi di avviso sono in uso contemporaneamente per più tabelle nel database, non chiarire la questione pg_try_advisory_xact_lock(tableoid::int, id): idessere univoci integerqui.
Poiché tableoidè una bigintquantità, può teoricamente traboccare integer. Se sei abbastanza paranoico, usa (tableoid::bigint % 2147483648)::intinvece - lasciando una "collisione dell'hash" teorica per i veri paranoici ...

Inoltre, Postgres è libero di testare le WHEREcondizioni in qualsiasi ordine. Si potrebbe testare pg_try_advisory_xact_lock()e acquisire un blocco prima status = 'standby' , che potrebbe comportare ulteriori serrature consultivo su righe indipendenti, quando status = 'standby'non è vero. Domanda correlata su SO:

In genere, puoi semplicemente ignorarlo. Per garantire che solo le righe qualificanti siano bloccate, è possibile nidificare i predicati in un CTE come sopra o una sottoquery con l' OFFSET 0hack (impedisce l'inline) . Esempio:

Oppure (più economico per le scansioni sequenziali) annidare le condizioni in CASEun'istruzione come:

WHERE  CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END

Tuttavia, il CASEtrucco impedirebbe anche a Postgres di utilizzare un indice status. Se un tale indice è disponibile, non è necessario nidificare ulteriormente per iniziare: solo le righe qualificanti verranno bloccate in una scansione dell'indice.

Dal momento che non puoi essere sicuro che un indice venga utilizzato in ogni chiamata, puoi semplicemente:

WHERE  status = 'standby'
AND    CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END

Il CASEè logicamente ridondante, ma i server lo scopo discusso.

Se il comando fa parte di una transazione lunga, considerare i blocchi a livello di sessione che possono essere (e devono essere) rilasciati manualmente. Quindi puoi sbloccare non appena hai finito con la riga bloccata: pg_try_advisory_lock()epg_advisory_unlock() . Il manuale:

Una volta acquisito a livello di sessione, viene mantenuto un blocco di avviso fino al rilascio esplicito o alla fine della sessione.

Relazionato:

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.