Lock di tabelle con PostgreSQL

banner

Gestire i lock in un database è un aspetto cruciale per garantire l’integrità dei dati e la performance delle applicazioni. In questo articolo, voglio condividere con voi alcune best practice per gestire i lock in PostgreSQL, evitando problemi di concorrenza e blocchi indesiderati.


Ho sempre pensato che la gestione dei lock in un database fosse un argomento complesso, ma fondamentale per garantire l’integrità dei dati e la performance delle applicazioni. In questo articolo, voglio condividere con voi alcune best practice per gestire i lock in PostgreSQL, evitando problemi di concorrenza e blocchi indesiderati.

Cosa sono i lock in PostgreSQL

I lock in PostgreSQL, ma più in generale di tutti i database, sono meccanismi utilizzati per controllare l’accesso concorrente alle risorse del database, come tabelle, righe o pagine. Quando un processo acquisisce un lock su una risorsa, impedisce ad altri processi di accedervi fino a quando il lock non viene rilasciato. Questo è essenziale per garantire l’integrità dei dati e prevenire condizioni di race, in cui più processi tentano di modificare gli stessi dati contemporaneamente, causando risultati imprevedibili o incoerenti.

Quando è necessario utilizzare i lock?

Partiamo dalla domanda fondamentale: quando ha senso utilizzare i lock in PostgreSQL? DI base, i lock sono necessari quando si devono eseguire operazioni che modificano i dati, come INSERT, UPDATE o DELETE, e si vuole garantire che nessun altro processo possa accedere agli stessi dati contemporaneamente. Questo è particolarmente importante in ambienti ad alta concorrenza, dove più utenti o processi possono accedere agli stessi dati.

Alcuni esempi di scenari reali in cui è necessario utilizzare i lock includono la gestione di transazioni che coinvolgono più tabelle, l’aggiornamento di record critici o la prevenzione di condizioni di race. Immagina, ad esempio, un sistema di prenotazione online: se due utenti cercano di prenotare lo stesso posto contemporaneamente, è essenziale utilizzare i lock per garantire che solo uno dei due possa completare la prenotazione, che di fatto si traduce in un’esperienza utente più fluida e affidabile.

Sebbene l’esperienza all’inizio mi facesse pensare che i lock fossero solo di due tipologie, ossia condivisi ed esclusivi, ho scoperto che PostgreSQL offre una varietà di lock più sofisticati, come i lock di riga, di tabella e di schema, ognuno con le proprie caratteristiche e utilizzi specifici. Ad esempio, i lock di riga consentono di bloccare solo le righe interessate da un’operazione, mentre i lock di tabella bloccano l’intera tabella, il che può essere utile in alcune situazioni ma può anche portare a problemi di concorrenza se non gestiti correttamente.

Tipi di lock in PostgreSQL

PostgreSQL offre diversi tipi di lock, ognuno con le proprie caratteristiche e utilizzi specifici. I principali tipi di lock includono:

  • Lock di tabella (Table Locks): bloccano l’intera tabella, impedendo a qualsiasi altro processo di accedervi finché il lock è attivo.
  • Lock di riga (Row Locks): consentono di bloccare solo le righe interessate da un’operazione, permettendo ad altri processi di accedere alle altre righe della stessa tabella.
  • Lock di pagina (Page Locks): bloccano una pagina specifica all’interno di una tabella, consentendo ad altri processi di accedere alle altre pagine.

Esaminiamoli uno per uno.

Lock di tabella (Table Locks)

I lock di tabella -indovinate- bloccano l’intera tabella, impedendo a qualsiasi altro processo di accedervi finché il lock è attivo. Questo tipo di lock è utile quando si devono eseguire operazioni che coinvolgono l’intera tabella, come ad esempio un’operazione di ALTER TABLE o una query che aggiorna un gran numero di righe. Questo vuol dire, niente INSERT, niente UPDATE o qualsiasi altra operazione che coinvolga quella tabella finché il lock è attivo.

Se vuoi toglierti qualche curiosità, puoi vedere quali lock di tabella ci sono utilizzando la vista pg_locks in PostgreSQL, che mostra tutte le lock attive nel sistema.

Di questi lock, esistono diverse modalità, come ACCESS SHARE, ROW SHARE, ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE e ACCESS EXCLUSIVE. Ognuna di queste modalità ha un livello di restrizione diverso, che determina quali operazioni possono essere eseguite dagli altri processi mentre il lock è attivo. Ad esempio, un lock di tipo ACCESS SHARE consente ad altri processi di leggere la tabella, ma impedisce qualsiasi operazione di scrittura, mentre un lock di tipo EXCLUSIVE impedisce qualsiasi operazione, sia di lettura che di scrittura, da parte di altri processi.

Per semplificarne la comprensione, riporto in una tabella le diverse modalità di lock di tabella e le operazioni consentite:

Modalità di LockOperazioni Consentite dagli Altri ProcessiAgisce a livello di…Descrizione
ACCESS SHARELettura (SELECT)TabellaConsente solo la lettura della tabella, impedendo qualsiasi operazione di scrittura.
ROW SHARELettura (SELECT)TabellaConsente la lettura e l’inserimento di righe, ma impedisce l’aggiornamento e la cancellazione.
ROW EXCLUSIVELettura (SELECT)TabellaConsente la lettura e l’inserimento di righe, ma impedisce qualsiasi operazione di scrittura che coinvolga la tabella.
SHARE UPDATE EXCLUSIVELettura (SELECT)TabellaConsente la lettura e l’aggiornamento di righe, ma impedisce l’inserimento e la cancellazione.
SHARELettura (SELECT)TabellaConsente la lettura e l’aggiornamento di righe, ma impedisce l’inserimento e la cancellazione.
SHARE ROW EXCLUSIVELettura (SELECT)TabellaConsente la lettura e l’aggiornamento di righe, ma impedisce l’inserimento e la cancellazione.
EXCLUSIVE*NessunaTabellaImpedisce qualsiasi operazione, sia di lettura che di scrittura, da parte di altri processi.
ACCESS EXCLUSIVE*NessunaTabellaImpedisce qualsiasi operazione, sia di lettura che di scrittura, da parte di altri processi.

Nella pratica, un comando come DROP TABLE o ALTER TABLE richiede un lock di tipo ACCESS EXCLUSIVE, che è il più restrittivo, mentre una query di SELECT richiede un lock di tipo ACCESS SHARE, che è il meno restrittivo.

  • La differenza tra EXCLUSIVE e ACCESS EXCLUSIVE è che il primo consente ad altri processi di acquisire lock di tipo ACCESS SHARE, mentre il secondo impedisce qualsiasi tipo di lock da parte di altri processi. Nella pratica, la differenza è sottile e spesso non rilevante, considerato che l’unico comando che richiede un lock di tipo EXCLUSIVE riguarda la gestione dell’aggiornamento delle viste materializzate concorrenti, che è un caso d’uso piuttosto specifico.

Ma come si aggiunge un lock?

Per aggiungere un lock a una tabella in PostgreSQL, è possibile utilizzare il comando LOCK. Ad esempio, per bloccare una tabella chiamata my_table in modalità EXCLUSIVE, si può eseguire la seguente query:

LOCK TABLE my_table IN EXCLUSIVE MODE;

Istruzione molto semplice: LOCK serve a indicare che vogliamo bloccare una tabella, TABLE specifica che stiamo bloccando una tabella (e non, ad esempio, una riga), my_table è il nome della tabella che vogliamo bloccare e IN EXCLUSIVE MODE indica la modalità di lock che vogliamo utilizzare.

Lock di riga (Row Locks)

I lock di riga consentono di bloccare solo le righe interessate da un’operazione, permettendo ad altri processi di accedere alle altre righe della stessa tabella. Questo tipo di lock è particolarmente utile quando si devono eseguire operazioni che coinvolgono solo un sottoinsieme di dati all’interno di una tabella, come ad esempio l’aggiornamento di un singolo record o la cancellazione di una riga specifica. I lock di riga sono gestiti automaticamente da PostgreSQL durante l’esecuzione di operazioni di scrittura, come INSERT, UPDATE o DELETE, e non richiedono l’uso del comando LOCK per essere attivati. Tuttavia, è possibile utilizzare il comando SELECT FOR UPDATE o SELECT FOR SHARE per acquisire esplicitamente un lock di riga durante una query di selezione, garantendo che nessun altro processo possa modificare le righe selezionate fino a quando il lock non viene rilasciato.

Un esempio pratico di utilizzo dei lock di riga è il seguente:

BEGIN;
SELECT * FROM my_table WHERE id = 1 FOR UPDATE;
-- Esegui operazioni di aggiornamento o cancellazione sulla riga con id = 1
COMMIT;

Questo permette di acquisire un lock di riga sulla riga con id = 1, impedendo ad altri processi di modificarla fino a quando la transazione non viene completata con COMMIT.

Altro esempio, se si vuole acquisire un lock di riga in modalità SHARE, si può utilizzare la seguente query:

BEGIN;
SELECT * FROM my_table WHERE id = 1 FOR SHARE;
-- Esegui operazioni di lettura sulla riga con id = 1
COMMIT;

Al contrario di prima, questo consente ad altri processi di acquisire lock di tipo SHARE sulla stessa riga, ma impedisce qualsiasi operazione di scrittura su di essa fino a quando la transazione non viene completata.

Nel caso dei lock di riga, ne esistono principalmente due modalità: FOR UPDATE e FOR SHARE. La modalità FOR UPDATE consente di acquisire un lock di riga in modalità esclusiva, impedendo a qualsiasi altro processo di modificare la riga fino a quando il lock non viene rilasciato, mentre la modalità FOR SHARE consente di acquisire un lock di riga in modalità condivisa, consentendo ad altri processi di acquisire lock di tipo SHARE sulla stessa riga, ma impedendo qualsiasi operazione di scrittura su di essa.

Esistono poi due versioni “specializzate” di lock che si possono utilizzare in combinazione con i lock di riga, ossia FOR NO KEY UPDATE e FOR KEY SHARE. La prima viene acquisita tramite UPDATE su colonne senza indice univoco, quindi è più debole di FOR UPDATE in quanto consente SELECT FOR KEY SHARE; la seconda agisce come FOR SHARE ma consente anche SELECT FOR UPDATE e SELECT FOR NO KEY UPDATE, quindi è più debole di FOR SHARE.

Lock di pagina (Page Locks)

I lock di pagina bloccano una pagina specifica all’interno di una tabella, consentendo ad altri processi di accedere alle altre pagine. Questo tipo di lock è gestito automaticamente da PostgreSQL durante l’esecuzione di operazioni di scrittura che coinvolgono più righe all’interno della stessa pagina, come ad esempio un’operazione di aggiornamento che modifica più record contigui. I lock di pagina sono meno restrittivi rispetto ai lock di tabella, ma possono comunque causare problemi di concorrenza se non gestiti correttamente, soprattutto in ambienti ad alta concorrenza.

Nota: prima di andare avanti… ricordiamo: cos’è una pagina? In PostgreSQL, una pagina è un’unità di memorizzazione che contiene un certo numero di righe di una tabella. La dimensione di una pagina è generalmente di 8 KB, e ogni pagina può contenere più righe a seconda della loro dimensione. I lock di pagina consentono di bloccare solo la pagina specifica che contiene le righe interessate da un’operazione, permettendo ad altri processi di accedere alle altre pagine della stessa tabella.

Per cui, se un’operazione di aggiornamento coinvolge più righe contigue che si trovano all’interno della stessa pagina, PostgreSQL acquisirà automaticamente un lock di pagina per garantire l’integrità dei dati durante l’operazione. Tutto sommato, i lock di pagina sono gestiti in modo trasparente da PostgreSQL e non richiedono l’uso del comando LOCK per essere attivati, ma è importante essere consapevoli della loro esistenza e del loro impatto sulla concorrenza, soprattutto in ambienti ad alta concorrenza.

E poi arrivano i Deadlock…

Abbiamo mentito: i lock non sono solo di 3 tipi, ma esistono anche i cosiddetti dead locks. Parliamo di deadlock quando due o più processi si bloccano reciprocamente, ciascuno in attesa che l’altro rilasci un lock di cui ha bisogno per continuare. Questo può accadere, ad esempio, quando due processi cercano di acquisire lock su risorse diverse in ordine inverso, creando una situazione di stallo in cui nessuno dei processi può procedere. I deadlock possono causare gravi problemi di performance e devono essere gestiti con attenzione. La realtà è che PostgreSQL ha un meccanismo di rilevamento dei deadlock integrato, che rileva automaticamente queste situazioni e risolve il problema terminando uno dei processi coinvolti, consentendo agli altri di continuare. Tuttavia, è importante progettare le transazioni in modo da minimizzare il rischio di deadlock, ad esempio acquisendo i lock in un ordine coerente ed evitando di mantenere i lock per periodi prolungati.

Nella pratica, se si dovesse verificare un deadlock, PostgreSQL restituirà un errore simile al seguente:

ERROR: deadlock detected
DETAIL: Process 12345 waits for ShareLock on transaction 67890; blocked by process 67890.
Process 67890 waits for ShareLock on transaction 12345; blocked by process 12345.
HINT: See server log for query details.

In questo caso, non c’è una configurazione magica per risolvere il problema, ma è necessario analizzare le transazioni coinvolte e identificare l’ordine in cui vengono acquisiti i lock, cercando di modificare il codice in modo da evitare situazioni di stallo. Ad esempio, se due processi stanno cercando di acquisire lock su risorse diverse in ordine inverso, è possibile modificare il codice in modo che entrambi i processi acquisiscano i lock nello stesso ordine, riducendo così il rischio di deadlock.

E quindi, come gestire i lock in modo efficiente?

Per gestire i lock in modo efficiente, è importante seguire alcune best practice. Innanzitutto, è fondamentale pianificare attentamente le transazioni e cercare di minimizzare la durata dei lock. Pensando a un flusso di lavoro come un cron che deve aggiornare dei dati su una tabella, è necessario chiedersi: “Quanto tempo impiegherà questa operazione? Posso ottimizzarla in modo da ridurre il tempo di lock?” Questo perché, a volte, il lock può non essere la risposta, o quantomeno non l’unica risposta, a un problema di concorrenza. In questi casi, potrebbe essere più efficace utilizzare tecniche come l’ottimizzazione delle query, la suddivisione dei dati in partizioni o l’uso di meccanismi di caching per ridurre la necessità di lock.

Evitare di mantenere i lock per periodi prolungati può aiutare a ridurre il rischio di blocchi indesiderati e migliorare la performance complessiva del sistema. Inoltre, è consigliabile utilizzare i lock in modo selettivo, bloccando solo le risorse necessarie e evitando di bloccare intere tabelle quando non è strettamente necessario. Un’altra best practice è quella di monitorare regolarmente i lock e le transazioni in corso, utilizzando strumenti come pg_stat_activity e pg_locks, per identificare eventuali problemi di concorrenza e intervenire tempestivamente.

Conclusioni

La gestione dei lock, come evidente, è un aspetto cruciale per garantire l’integrità dei dati e la performance delle applicazioni in PostgreSQL. Utilizzare i lock in modo efficiente richiede una buona comprensione dei diversi tipi di lock disponibili, delle situazioni in cui è necessario utilizzarli e delle best practice per minimizzare il rischio di blocchi indesiderati e deadlock. Seguendo queste linee guida, è possibile gestire i lock in modo efficace, migliorando la concorrenza e la performance del sistema nel suo complesso.

Risorse utili

Bio autore

Serena Sensini - Ciao! Mi chiamo Serena Sensini e sono la creatrice di @ TheRedCode.

TheRedCode.it - Il mondo #tech a piccoli #bit

Partners

Community, aziende e persone che supportano attivamente il blog

Logo di Welyk
Logo di GrUSP
Logo di Python Milano
Logo di Schrodinger Hat
Logo di Python Biella Group
Logo di Fuzzy Brains
Logo di Django Girls Italy
Logo di Improove
Logo de Il Libro Open Source
Logo di NgRome
Logo de La Locanda del Tech
Logo di Tomorrow Devs
Logo di DevDojo

Vuoi diventare tech content creator? 🖊️

Se ti va di raccontare la tua esperienza nel mondo tech, questo è il posto giusto.

Cerchiamo voci autentiche, esempi pratici e punti di vista utili per chi legge.

Scrivici a collaborazioni[at]theredcode.it con una proposta: idea, taglio del contenuto e una breve presentazione. Non vediamo l'ora di leggere la tua esperienza!

Invia la tua idea