Redis in Laravel: beyond caching - code, pub/sub e session management avanzato
Redis è il componente infrastrutturale che trovo più sottoutilizzato nelle applicazioni Laravel dei clienti PMI. Nella grande maggioranza dei casi, Redis è installato, configurato come driver di cache nel .env (CACHE_DRIVER=redis), e usato esclusivamente per il caching dei risultati di query con Cache::remember(). È come avere una Ferrari e usarla solo per andare al supermercato - funziona, ma stai ignorando il 90% delle capacità dello strumento. Redis non è solo una cache: è un data store in-memory con strutture dati native (liste, set, sorted set, hash, stream) che lo rendono uno strumento universale per code di messaggi, session management, rate limiting, pub/sub, lock distribuiti e contatori atomici - tutti pattern che un'applicazione Laravel ad alto traffico ha bisogno di implementare e che con Redis si implementano in poche righe di codice con prestazioni di microsecondi.
A luglio 2025 ho ristrutturato l'uso di Redis per un'applicazione di prenotazioni di un cliente del settore servizi - un portale con 200 utenti attivi e picchi di 500 richieste al secondo durante le finestre di prenotazione aperte (slot che si esaurivano in 2-3 minuti). L'applicazione usava Redis solo per la cache, le code giravano su database (il driver database di Laravel Queue), le sessioni erano su file, e il rate limiting era basato sul middleware throttle con contatore in cache. Dopo la ristrutturazione - code su Redis con priority, sessioni su Redis con TTL personalizzato, rate limiting con Redis sliding window, e pub/sub per le notifiche real-time di slot disponibili - il tempo di risposta mediano durante i picchi è sceso da 1.200 ms a 180 ms e il tasso di errore dal 8% allo 0,1%.
Come funzionano le code Redis in Laravel e perché sono superiori alle code su database?
La differenza fondamentale tra il driver database e il driver redis per le code Laravel è nel meccanismo di polling. Con il driver database, il worker esegue una query SQL (SELECT ... FOR UPDATE) sul database applicativo ogni secondo per verificare se ci sono job da processare - una query che, sotto carico, compete con le query dell'applicazione per le connessioni MySQL e per i lock sulle tabelle. Con il driver Redis, il worker usa il comando BLPOP - un blocking pop che sospende il worker fino a quando un job non è disponibile nella lista, senza polling e senza caricare il database. La differenza di prestazioni è di due ordini di grandezza: il polling su database aggiunge 1-5 ms di latenza per job e consuma una connessione MySQL permanente per worker; il BLPOP su Redis aggiunge 0,01 ms e non tocca il database.
Per l'applicazione di prenotazioni, la migrazione delle code da database a Redis ha avuto un impatto immediato: i worker processavano i job di conferma prenotazione in 2 ms (Redis) invece di 15 ms (database), il che significava che durante i picchi di 500 prenotazioni al minuto, i job venivano processati in tempo reale invece di accumularsi in coda. Ma il vantaggio più significativo è stato l'eliminazione della contention sul database: con il driver database e 10 worker attivi, ogni worker teneva una connessione MySQL aperta permanentemente per il polling - 10 connessioni in più che competevano con le connessioni dell'applicazione web. Con Redis, quelle 10 connessioni MySQL sono state liberate, riducendo il tempo di risposta delle query applicative del 15% durante i picchi.
Nel mio profilo professionale trovi il dettaglio dell'esperienza che porto nell'ottimizzazione di stack Laravel con Redis - un'area dove la configurazione corretta produce miglioramenti di performance che nessuna ottimizzazione del codice PHP può eguagliare, perché il collo di bottiglia non è nel codice ma nell'infrastruttura di comunicazione tra i componenti.
Code con priorità: i Sorted Set di Redis per il triage dei job
Laravel supporta le code con priorità nativamente - puoi dichiarare dispatch($job)->onQueue('high') e configurare il worker per processare prima la coda high e poi la coda default. Ma il meccanismo di default è una selezione round-robin tra le code: il worker controlla high, se vuota controlla default, processa un job, e ricomincia. Se la coda default ha 1.000 job e la coda high ne riceve uno durante l'elaborazione, il worker non se ne accorge fino a quando non ha terminato il job corrente e ha rifatto il ciclo. Per l'applicazione di prenotazioni, dove i job di conferma slot (priorità alta) dovevano essere processati prima dei job di notifica email (priorità bassa), serviva un meccanismo di preemption più aggressivo.
La soluzione è usare i Sorted Set di Redis come struttura dati nativa per le code con priorità. Un Sorted Set è un set dove ogni elemento ha un score numerico - e gli elementi vengono restituiti ordinati per score. Assegnando uno score più basso ai job ad alta priorità (score 1 per critical, 5 per high, 10 per default, 20 per low), il comando ZPOPMIN restituisce sempre il job con la priorità più alta, indipendentemente dall'ordine di inserimento. La configurazione in Laravel con il pacchetto Horizon è diretta: Horizon supporta le code con priorità multiple e permette di configurare quanti worker dedicare a ciascuna coda.
Per il portale di prenotazioni, la configurazione Horizon con priority era: 4 worker dedicati alla coda conferma-slot (priorità critica, TTL 5 secondi), 2 worker alla coda notifiche (priorità alta, TTL 30 secondi), e 2 worker alla coda reportistica (priorità bassa, TTL 5 minuti). Durante i picchi di prenotazione, i 4 worker della coda conferma processavano 200 job al minuto con latenza sotto i 5 ms, mentre i job di notifica e reportistica venivano accumulati e processati dopo il picco. Il risultato: la conferma dello slot era istantanea per l'utente (percepita come sincrona), e la notifica email arrivava 1-2 minuti dopo - un ritardo accettabile per un'email ma inaccettabile per la conferma della prenotazione.
Session management su Redis: prestazioni e scalabilità
Il terzo uso di Redis che implemento come standard su ogni applicazione Laravel ad alto traffico è il session management. Il driver file di default salva le sessioni come file nel filesystem - un approccio che funziona per applicazioni con traffico basso ma che diventa un collo di bottiglia quando il traffico cresce: ogni richiesta HTTP legge e scrive un file di sessione, generando I/O su disco proporzionale al numero di richieste. Su un VPS con disco NVMe, l'overhead è trascurabile fino a qualche centinaio di sessioni simultanee; su un server con storage condiviso o virtualizzato, l'overhead diventa misurabile oltre le 100 sessioni simultanee.
Con Redis, le sessioni sono in memoria - lettura e scrittura in microsecondi, nessun I/O su disco, nessuna contention. La configurazione è una riga nel .env: SESSION_DRIVER=redis. Ma il beneficio più importante non è la velocità - è la scalabilità orizzontale. Se l'applicazione gira su più server dietro un load balancer, le sessioni su file sono locali a ciascun server: un utente che viene instradato al server A nella prima richiesta e al server B nella seconda perde la sessione. Le sessioni su Redis sono condivise tra tutti i server - il load balancer può distribuire le richieste liberamente senza sticky session, semplificando l'architettura e migliorando la distribuzione del carico.
Per l'applicazione di prenotazioni, la migrazione delle sessioni da file a Redis ha prodotto due benefici misurabili: il tempo di risposta mediano è sceso di 8 ms (eliminazione dell'I/O su disco per la sessione), e la configurazione del load balancer è stata semplificata eliminando la necessità di sticky session - il che ha migliorato la distribuzione del carico tra i due server e ha ridotto il rischio di sovraccarico asimmetrico durante i picchi.
Pub/Sub per le notifiche real-time: quando Reverb non serve
Per le notifiche real-time nell'applicazione di prenotazioni - aggiornamento del contatore di slot disponibili nella pagina di prenotazione - ho usato Redis Pub/Sub integrato con Laravel Reverb come trasporto per gli eventi broadcast. Reverb usa Redis come backbone per la distribuzione degli eventi tra le istanze del server WebSocket - quando un utente completa una prenotazione, il controller pubblica un evento SlotPrenotato su Redis, Reverb lo riceve e lo inoltra a tutti i client WebSocket collegati alla pagina di prenotazione, e il contatore di slot disponibili si aggiorna in tempo reale senza refresh della pagina.
Ma in scenari dove la notifica real-time non richiede un WebSocket persistente - ad esempio, un banner che mostra "Ultimi 3 slot disponibili!" aggiornato ogni 5 secondi - Redis Pub/Sub può essere usato direttamente con un polling HTTP leggero: il client fa una richiesta GET /api/slots/conteggio ogni 5 secondi, il server legge il conteggio da una chiave Redis aggiornata atomicamente con DECRBY ad ogni prenotazione, e la risposta è in meno di 1 ms perché non tocca il database. Questo approccio è più semplice di Reverb (nessun WebSocket, nessun processo server aggiuntivo) e sufficiente per casi dove la latenza di 5 secondi è accettabile.
Lock distribuiti con Redis: prevenire la concorrenza sui dati critici
L'ultimo pattern Redis che uso regolarmente in Laravel è il distributed lock - un meccanismo che garantisce che solo un processo alla volta possa eseguire un'operazione critica, anche quando l'applicazione gira su più server o con più worker in parallelo. Il caso d'uso classico è la prenotazione di uno slot: se due utenti cliccano "Prenota" sullo stesso slot nello stesso istante, senza un lock distribuito entrambe le richieste leggono "slot disponibile" dal database, entrambe procedono con la prenotazione, e il risultato è un overbooking - due prenotazioni per lo stesso slot.
Laravel fornisce il metodo Cache::lock() che implementa un lock distribuito usando il comando SET NX EX di Redis: il lock viene acquisito solo se non esiste già (NX = "not exists"), con un timeout automatico (EX = "expire") per evitare che un processo crashato lasci il lock permanentemente bloccato. Il pattern nel controller di prenotazione è semplice ma critico: acquisisci il lock sullo slot, verifica la disponibilità, processa la prenotazione, rilascia il lock. Se il lock non è disponibile (un altro processo sta già prenotando lo stesso slot), il tentativo viene ritentato dopo un breve backoff o restituisce un errore esplicito all'utente.
Per l'applicazione di prenotazioni, i lock Redis hanno eliminato completamente il problema dell'overbooking che si verificava 2-3 volte alla settimana durante i picchi - ogni slot viene prenotato da un solo utente alla volta, con gli altri che ricevono un feedback immediato ("Slot in fase di prenotazione da un altro utente, riprova tra pochi secondi") invece di un overbooking scoperto dopo il pagamento. La latenza del lock Redis è di circa 0,5 ms per l'acquisizione e 0,3 ms per il rilascio - un overhead invisibile che previene un problema di business significativo.
L'alternativa al lock Redis - il SELECT ... FOR UPDATE di MySQL - funziona ma ha due svantaggi: primo, il lock vive all'interno della transazione database e se la transazione dura a lungo (perché include una chiamata HTTP al gateway di pagamento) la riga del database resta bloccata per tutta la durata, impedendo anche la sola lettura dello stato dello slot; secondo, il lock database non funziona in architetture dove lo slot è gestito da un microservizio separato dal database delle prenotazioni. Il lock Redis è indipendente dal database, ha un timeout configurabile, e funziona cross-servizio - è la scelta corretta per qualsiasi operazione critica che richiede mutual exclusion in un'architettura distribuita.
Redis è uno strumento che ripaga l'investimento di apprendimento molte volte - ogni nuova struttura dati che impari (liste per le code, sorted set per le priorità, hash per le sessioni, pub/sub per gli eventi) è un pattern architetturale in più nella tua cassetta degli attrezzi. Se la tua applicazione Laravel usa Redis solo come cache e le code girano su database, contattami per un assessment di ottimizzazione: in mezza giornata analizziamo il workload, migriamo le code e le sessioni su Redis, e misuriamo il delta di prestazioni - un investimento che si ripaga nel primo picco di traffico.