Sincronizzazione di applicazioni distribuite con Redis

La libreria Redisson fornisce un meccanismo di distributed locking che consente di coordinare l’esecuzione di metodi o sezioni critiche di codice tra più thread o nodi.

La sincronizzazione dei processi concorrenti rappresenta un aspetto cruciale nello sviluppo di applicazioni distribuite. Quando più istanze di un’applicazione eseguono operazioni che accedono a risorse condivise, è necessario garantire che tali operazioni avvengano in modo ordinato, evitando conflitti e inconsistenze.

In questo contesto, strumenti come Redis e, in particolare, la libreria Redisson, forniscono un meccanismo di distributed locking che consente di coordinare l’esecuzione di metodi o sezioni critiche di codice tra più thread o nodi.

Il contesto: sincronizzazione FIFO con Redisson e ActiveMQ

Nel progetto applicativo in questione, sviluppato in Java, viene utilizzato Redisson come client Redis per implementare un sistema di lock distribuiti.
L’applicativo è distribuito su tre nodi distinti, tra loro indipendenti, ma che condividono risorse comuni come il database e i sistemi di messaggistica basati su ActiveMQ.

L’obiettivo principale dell’introduzione dei lock è quello di:

  • ordinare e sincronizzare il processing dei messaggi ricevuti attraverso le code;
  • garantire un accesso ordinato ed esclusivo a determinate risorse condivise;
  • prevenire l’elaborazione concorrente di operazioni che devono essere eseguite in sequenza;
  • assicurare che, in caso di errore o timeout, il lock venga rilasciato automaticamente dopo un tempo prestabilito (leaseTime).

Un prerequisito fondamentale del progetto è che i messaggi vengano processati seguendo una logica FIFO (First In, First Out). Sebbene il gestore delle code garantisca l’ordine di consegna dei messaggi, la presenza di più nodi in ascolto sulla stessa coda introduce una criticità: non è garantito che l’elaborazione dei messaggi avvenga effettivamente nello stesso ordine di arrivo.

Ad esempio, un nodo potrebbe ricevere il messaggio 1, mentre un altro nodo riceve immediatamente dopo il messaggio 2. Poiché i due nodi lavorano in parallelo, non vi è garanzia che il messaggio 1 venga elaborato prima del messaggio 2.

È in questo contesto che si è resa necessaria l’implementazione di un sistema di locking distribuito affidabile, basato su Redis e gestito tramite Redisson, in grado di coordinare i nodi e garantire l’ordine corretto di elaborazione.

Le problematiche: dal RLock al Fair Lock, il blocco permanente

Il primo approccio all’utilizzo di Redis ha previsto l’implementazione di un meccanismo di lock distribuito tramite l’oggetto RLock fornito da Redisson. Questo tipo di lock, basato su una chiave predefinita, consente di ottenere un accesso sincronizzato a specifici blocchi di codice e risorse condivise, impedendo che più thread o nodi eseguano simultaneamente le stesse operazioni.

Tuttavia, questo approccio iniziale non si è rivelato pienamente sufficiente. Il problema principale risiedeva nella mancanza di garanzia sulla sequenzialità di accesso dei thread che attendevano il rilascio del lock. In pratica, quando un thread tentava di acquisire un lock già occupato, esso rimaneva in attesa finché il lock non veniva liberato. Durante questo periodo, anche altri thread potevano mettersi in attesa dello stesso lock; tuttavia, al momento del rilascio, non era garantito che il primo thread in attesa fosse il primo a ottenere l’accesso, compromettendo la logica FIFO necessaria al sistema.

Per risolvere questa limitazione, è stato adottato il meccanismo di fair lock (RLockFair), che assicura un ordine di accesso equo tra i thread in coda.
Inoltre, è stato introdotto un leaseTime per garantire la liberazione automatica del lock dopo un determinato intervallo di tempo, anche in caso di errore o mancato rilascio esplicito da parte del thread.

Nonostante queste migliorie, durante il normale funzionamento del sistema si è riscontrato che, in alcune circostanze, il lock risultava permanentemente occupato, impedendo l’esecuzione delle operazioni successive.
 
Questo comportamento anomalo si verificava anche quando il leaseTime era stato correttamente impostato, condizione che avrebbe dovuto assicurare il rilascio automatico del lock allo scadere del tempo.

La difficoltà principale consisteva nell’impossibilità di identificare quale thread o istanza dell’applicazione mantenesse il lock in stato di occupazione, rendendo complessa l’attività di diagnosi e risoluzione del problema. In sostanza, il sistema appariva come se “qualcuno” avesse acquisito il lock e non lo avesse mai rilasciato — ma senza un modo chiaro per capire chi fosse quel “qualcuno”.

Obiettivi: diagnosticare il lock Redisson e garantire il leaseTime

Gli obiettivi da raggiungere erano i seguenti:

  • Comprendere la causa del blocco e verificare se il leaseTime venisse effettivamente applicato.
  • Individuare quale client o thread detenesse il lock in un dato momento.
  • Implementare un meccanismo di monitoraggio e logging che permettesse di diagnosticare più facilmente problemi di lock futuri.
  • Assicurare la corretta liberazione del lock in tutti i percorsi di esecuzione, inclusi quelli che terminano con eccezioni.

Soluzione implementata: gestione del leaseTime e ownership del lock in Redisson

L’analisi ha portato alla scoperta che Redisson memorizza, all’interno della chiave Redis corrispondente al lock, una stringa che identifica univocamente il client e il thread che lo ha acquisito, nel formato:

Shell

<client-uuid>:<thread-id>

Sfruttando questa informazione, è stato possibile:

- leggere direttamente da Redis (es. tramite GET lockKey) il valore del lock per conoscere quale istanza lo detiene;

- recuperare in codice Java le stesse informazioni, utilizzando:

Java

String owner = (String) redisson.getBucket(lockKey).get();
System.out.println("Lock held by: " + owner);

ottenere l’UUID del client locale con:

Java

redisson.getId();

- loggare sistematicamente l’acquisizione e il rilascio del lock con informazioni su client ID, thread ID, timestamp e durata prevista del leaseTime.

Durante l’analisi è emerso anche un aspetto fondamentale del comportamento di Redisson: il lock può essere rilasciato esclusivamente dal thread che lo ha acquisito. Infatti, Redisson lega ogni lock al threadId del thread chiamante; se un altro thread tenta di rilasciarlo, il sistema non lo riconosce come legittimo proprietario e il lock rimane attivo fino alla scadenza del leaseTime.

Per questo motivo è essenziale che:

  • l’istanza di RLock venga acceduta e gestita sempre dallo stesso thread;
  • le operazioni di lock() e unlock() avvengano nello stesso contesto di esecuzione;
  • e che l’invocazione di unlock() sia sempre racchiusa in un blocco finally, come nel seguente esempio:

Inoltre, è stato verificato che alcune sezioni di codice utilizzavano lock() senza specificare leaseTime, attivando così il watchdog interno di Redisson, che estende indefinitamente la durata del lock finché il thread è attivo.

Uniformando le chiamate a lock(leaseTime, TimeUnit.SECONDS) e garantendo la presenza del blocco try-finally per l’invocazione di unlock(), il problema di persistenza dei lock è stato risolto.

I risultati: un sistema di blocco distribuito affidabile e monitorabile

Dopo l’adozione della soluzione:

  • è stato possibile identificare chiaramente quale istanza e quale thread detenevano un lock in qualsiasi momento;
  • i lock non restano più pendenti oltre il tempo massimo stabilito;
  • l’aggiunta di log dettagliati ha reso la diagnosi di eventuali anomalie molto più immediata;
  • la gestione uniforme del leaseTime ha eliminato la possibilità che il watchdog mantenesse i lock oltre il necessario.

In sintesi, la stabilità e la prevedibilità del sistema di sincronizzazione sono state significativamente migliorate.

L’analisi del problema ha evidenziato l’importanza dell’utilizzo di un sistema di distributed locking per le applicazioni distribuite, e soprattutto la comprensione approfondita delle librerie come Redisson che offrono questa soluzione.

La libreria infatti offre diversi strumenti di locking in base alle esigenze, ed è necessaria un’analisi approfondita a priori dell’offerta della libreria prima di cimentarsi nell’implementazione di una soluzione. I vari malfunzionamenti riscontrati non erano causati da un errore della libreria, ma dall’utilizzo dei tool errati, da una configurazione non omogenea e ad una mancanza di strumenti di monitoraggio.

Utilizzando il fair locking, Implementando un sistema di logging e tracciamento dell’ownership dei lock, è stato possibile non solo risolvere il problema, ma anche dotare l’applicazione di un livello di osservabilità superiore, utile per la manutenzione futura.
Questo intervento ha migliorato l’affidabilità complessiva del sistema e ha fornito un metodo sistematico per la gestione e la diagnostica dei lock distribuiti.

 

Ti è piaciuto quanto hai letto? Iscriviti a MISPECIAL, la nostra newsletter, per ricevere altri interessanti contenuti.

Iscriviti a MISPECIAL

 

Contenuti simili
DIGITAL ENTERPRISE
ott 14, 2025

AI TRiSM framework per la gestione della fiducia, del rischio e della sicurezza dell'intelligenza artificiale

DIGITAL ENTERPRISE
set 01, 2025

Come gestire le dipendenze OSGi su Liferay 7.4. Questa guida pratica ti mostra come risolvere i conflitti e integrare librerie di terze parti nei tuoi moduli, con esempi concreti come Apache POI.