In breve. La scelta fra Pub/Sub e Streams in Redis si riduce a una domanda: che cosa deve succedere al messaggio se, nel momento in cui lo pubblichi, nessuno lo sta leggendo? Con Pub/Sub la risposta è «sparisce»: la documentazione parla di consegna at-most-once, cioè al più una volta, e PUBLISH ti dice quanti ascoltatori ha raggiunto (nei miei esperimenti, 0 quando nessuno era connesso). Con Streams il messaggio resta nel flusso finché non lo tagli, e un gruppo di consumatori ricorda quali messaggi ha consegnato e quali no: se un consumatore muore a metà, il messaggio resta «in attesa» e un altro può reclamarlo. Il prezzo è che con Streams devi confermare ogni messaggio (XACK), controllare la coda delle conferme mancanti e decidere quando tagliare il flusso, altrimenti cresce senza limite. Qui sotto trovi le due strategie eseguite su Redis 8.0.5 e 7.4.7, con le uscite vere. C'è anche una nota di onestà: il backend di questo sito oggi non usa né l'una né l'altra, e ti dico perché l'articolo esiste lo stesso.
Metodo. Tutti i comandi sono stati eseguiti l'8 novembre 2026 su istanze usa-e-getta (porta 6391, Redis 8.0.5; un contenitore Redis 7.4.7 per le ripetizioni). Dove le uscite coincidono mostro una sola; dove cambiano lo dico.
Revisione del 13 novembre 2026. In questa revisione non ho avviato nessun server: le uscite dei blocchi sono quelle dell'8 novembre e non le ho riprodotte. Un controllo senza server lo permette un solo dato: gli identificativi dei messaggi iniziano con un timestamp in millisecondi, e 1794122474177 corrisponde all'8 novembre 2026, 07:21 UTC, cioè al giorno della prova. Il resto l'ho riletto sulla documentazione ufficiale di oggi, e ciò che non è nelle mie uscite lo dichiaro «non eseguito».
Il problema: un messaggio senza lettore
Un servizio produce un evento («ordine confermato») e un altro deve reagire. Finché entrambi sono accesi sembra che qualunque meccanismo funzioni. Il difetto compare il giorno in cui il lettore è fermo per un deploy, in riavvio o scollegato per un istante di rete: allora la scelta fatta in fase di progetto decide se perdi l'evento o se lo ritrovi al ritorno. Redis ti dà due strumenti con due risposte opposte, ed è utile averle viste entrambe prima di sceglierne una per abitudine.
Pub/Sub: il messaggio vive finché c'è chi lo ascolta
Pubblicare senza nessun ascoltatore
Il primo esperimento è il più semplice: pubblico su un canale che nessuno ha sottoscritto.
$ redis-cli -p 6391 publish ordini "ordine-1"
0
La risposta è 0: il numero di client che hanno ricevuto il messaggio. Redis non ha scritto nulla da nessuna parte, quindi il messaggio non esiste più. Nessun errore, nessun avviso: se non guardi il valore di ritorno, non te ne accorgi.
Con un ascoltatore connesso, e dopo che si è disconnesso
Ora sottoscrivo un canale in background, scrivendo su file quello che arriva, pubblico, scollego l'ascoltatore, pubblico di nuovo e lo ricollego:
$ redis-cli -p 6391 subscribe ordini > ricevuti.txt & # ascoltatore connesso
$ redis-cli -p 6391 publish ordini "ordine-2"
1
$ kill %1 # l'ascoltatore si disconnette
$ redis-cli -p 6391 publish ordini "ordine-3"
0
$ redis-cli -p 6391 subscribe ordini > ricevuti2.txt & # si ricollega
$ cat ricevuti.txt ricevuti2.txt
subscribe
ordini
1
message
ordini
ordine-2
subscribe
ordini
1
Il primo ascoltatore ha ricevuto ordine-2. L'ordine-3, pubblicato mentre non c'era nessuno, non arriva a nessuno, nemmeno al ricollegamento: nel secondo file c'è solo la conferma di sottoscrizione. Su 7.4.7 il risultato è identico.
Cosa promette la documentazione
Una variante utile è la sottoscrizione per pattern: con PSUBSCRIBE un ascoltatore riceve tutti i canali che corrispondono a un modello. Il valore di ritorno di PUBLISH conta i destinatari anche qui:
$ redis-cli -p 6391 psubscribe 'ordini:*' > pat.txt &
$ redis-cli -p 6391 publish ordini:italia "a"
1
$ redis-cli -p 6391 publish ordini:francia "b"
1
$ redis-cli -p 6391 publish fatture "c"
0
$ cat pat.txt
psubscribe
ordini:*
1
pmessage
ordini:*
ordini:italia
a
pmessage
ordini:*
ordini:francia
b
Il canale fatture non corrisponde al modello e il suo PUBLISH torna 0: di nuovo, nessun errore.
Non è un difetto da correggere, è il contratto. La pagina di Redis sul Pub/Sub lo dice con parole esplicite: la consegna ha semantica at-most-once, un messaggio è consegnato una volta se lo è, e se il sottoscrittore non riesce a gestirlo (un errore, una disconnessione di rete) non c'è modo che sia inviato di nuovo. Quando va bene:
- Il dato è una notifica effimera che perde valore subito: presenza online, un cursore, un aggiornamento che un client riceverà di nuovo in forma completa al prossimo ciclo.
- Hai molti ascoltatori e ognuno deve vedere ogni messaggio: il Pub/Sub fa broadcast.
- Puoi tollerare perdite e hai un modo per riallinearti (per esempio rileggere lo stato dal database alla riconnessione).
Le tre condizioni non sono alternative, si sostengono: la terza, un modo di riallinearsi, è quella che rende accettabili le prime due. Senza rileggere lo stato da una fonte affidabile, una perdita silenziosa diventa un dato sbagliato che nessuno nota. Se non sai scrivere in una riga come il tuo client si riallinea dopo una riconnessione, il tuo non è un caso da Pub/Sub, anche quando le prime due voci ti descrivono.
Streams: un registro che ricorda chi ha letto cosa
Il flusso conserva anche senza consumatori
Uno stream è un log di sola aggiunta. Scrivo due ordini quando nessun consumatore esiste:
$ redis-cli -p 6391 xadd ordini '*' id 1 totale 40
1794122474177-0
$ redis-cli -p 6391 xadd ordini '*' id 2 totale 25
1794122474191-0
$ redis-cli -p 6391 xlen ordini
2
XADD restituisce l'identificativo del messaggio, che di default è un timestamp in millisecondi più un contatore. I due messaggi sono lì e ci resteranno. La differenza con il caso precedente è tutta qui: la scrittura non dipende da chi legge.
Rileggere lo storico
Un'altra cosa che Pub/Sub non può fare: leggere i messaggi che hai già letto. Con XRANGE scorri il flusso per intervalli di identificativo, e - e + significano «dal più vecchio al più nuovo»:
$ redis-cli -p 6391 xrange ordini - +
1794122474177-0
id
1
totale
40
1794122474191-0
id
2
totale
25
Gli identificativi sono gli stessi due che XADD aveva restituito, nello stesso ordine: è l'invariante da cui parti quando devi capire se un consumatore ha letto tutto. Ricostruire lo stato dopo un incidente, rielaborare gli eventi con una logica corretta, aggiungere un nuovo consumatore che parte dal passato: sono tutte operazioni che hanno senso solo se il messaggio sopravvive alla lettura. Se il tuo caso non ne ha bisogno, questa capacità è peso che ti porti dietro.
Il gruppo di consumatori e l'insidia del $
Un consumer group dà a più consumatori un cursore condiviso: ogni messaggio va a uno solo di loro, e il gruppo tiene traccia di quello che ha consegnato. Si crea con XGROUP CREATE, e l'ultimo argomento decide da dove si parte. Con 0 il gruppo legge tutto lo storico; con $ parte dal fondo, cioè vede solo i messaggi che arrivano dopo:
$ redis-cli -p 6391 xgroup create ordini nuovo '$'
OK
$ redis-cli -p 6391 xreadgroup group nuovo a count 10 streams ordini '>'
# nessun messaggio: redis-cli stampa una riga vuota (risposta nulla)
Quindi un gruppo creato con $ dopo che il flusso contiene già dati non vede niente. Stesso comportamento su 7.4.7. È un errore comune quando si crea il gruppo nello stesso deploy del consumatore: i messaggi scritti prima non arrivano mai. Per riprendere lo storico usa 0.
Il consumatore che legge e muore
Ora il caso che il Pub/Sub non può reggere. Creo un gruppo da 0, il consumatore a legge un messaggio e «muore» senza confermarlo; il consumatore b legge il successivo e lo conferma con XACK:
$ redis-cli -p 6391 xgroup create ordini contabilita 0
OK
$ redis-cli -p 6391 xreadgroup group contabilita a count 1 streams ordini '>'
ordini
1794122474177-0
id
1
totale
40
$ redis-cli -p 6391 xreadgroup group contabilita b count 1 streams ordini '>'
ordini
1794122474191-0
id
2
totale
25
$ redis-cli -p 6391 xack ordini contabilita 1794122474191-0
1
$ redis-cli -p 6391 xpending ordini contabilita
1
1794122474177-0
1794122474177-0
a
1
L'ultima chiamata, XPENDING, risponde con quattro cose in sequenza: il numero di messaggi consegnati e non confermati (1), l'identificativo più basso e più alto fra questi (qui coincidono) e il consumatore che li ha in mano con il suo conteggio (a, 1). Il messaggio di a non è andato perso: è nella lista delle conferme mancanti, e resta lì finché qualcuno non lo confermerà o lo reclamerà.
Come leggere il risultato di XPENDING
La forma riassuntiva restituisce quattro informazioni, e conviene saperle riconoscere perché redis-cli, quando non è collegato a un terminale, le stampa una per riga senza etichette:
- Il numero totale di messaggi consegnati e non ancora confermati (prima riga).
- L'identificativo del più vecchio fra essi.
- L'identificativo del più recente.
- Per ciascun consumatore che ne ha in mano, il suo nome e il suo conteggio.
Se aggiungi gli argomenti - + 10 ottieni il dettaglio per messaggio: consumatore, millisecondi di inattività, numero di consegne. È il dettaglio che userai in un allarme: quanti messaggi sono in sospeso da più di un minuto, quale consumatore li tiene.
Reclamare il lavoro di chi è morto
Dopo un tempo di inattività scelto da te, un altro consumatore può prendere in carico i messaggi rimasti in sospeso con XAUTOCLAIM. Attendo un secondo e mezzo e reclamo con una soglia di 1000 ms. Il comando risponde con il cursore da cui riprendere (0-0 vuol dire che ha scandito tutto) e con i messaggi reclamati:
$ redis-cli -p 6391 xautoclaim ordini contabilita b 1000 0-0 count 10
0-0
1794122474177-0
id
1
totale
40
$ redis-cli -p 6391 xpending ordini contabilita - + 10
1794122474177-0
b
18
2
$ redis-cli -p 6391 xack ordini contabilita 1794122474177-0
1
$ redis-cli -p 6391 xpending ordini contabilita
0
$ redis-cli -p 6391 xlen ordini
2
Il messaggio ora è di b, inattivo da 18 ms, e il contatore delle consegne è salito a 2. Dopo il XACK finale la lista delle conferme mancanti è vuota: XPENDING risponde 0 e poi tre righe vuote, che sono i tre campi nulli della forma riassuntiva. Quel contatore è il segnale da guardare: un messaggio che arriva a 5, 6, 7 consegne è un messaggio che nessuno riesce a elaborare, e serve una coda dei messaggi morti o un'allerta, non un altro tentativo infinito. La stessa sequenza su Redis 7.4.7 dà le stesse risposte, con il solo tempo di inattività diverso (6 ms): dipende da quanto tempo passa fra i due comandi, non dalla versione.
La documentazione corrente degli Streams descrive anche XNACK, che restituisce al gruppo i messaggi in sospeso senza confermarli, così un altro consumatore li riceve subito senza aspettare la soglia di inattività: la tabella dei comandi lo indica dalla 8.8.0. Non l'ho eseguito, perché le mie istanze sono 8.0.5 e 7.4.7 e non lo hanno: lo cito dalla pagina, e se lavori su una versione più vecchia XAUTOCLAIM resta l'unico modo di recuperare il lavoro.
Il flusso cresce: tagliarlo è una scelta
Confermare un messaggio non lo cancella: dopo XACK il flusso aveva ancora i due messaggi (XLEN = 2 nei miei test). Senza un taglio, la memoria cresce con ogni evento. Il taglio si fa alla scrittura con MAXLEN:
$ for i in $(seq 1 1000); do echo "XADD log MAXLEN = 100 * n $i"; done | redis-cli -p 6391 --pipe | tail -1
errors: 0, replies: 1000
$ redis-cli -p 6391 xlen log
100
Mille scritture, cento messaggi conservati. Nella sintassi di XADD il simbolo prima della soglia può essere = (taglio esatto, quello che ho usato) oppure ~ (approssimato). Il trade-off è esplicito: tagli per dimensione e puoi buttare un messaggio che un consumatore lento non ha ancora letto. Per questo conviene misurare quanto resta indietro il gruppo prima di fissare il limite.
Tre eventi, tre scelte (ragionamento, non misura)
Per fissare le idee, tre eventi immaginari di un backend, con la risposta che darei io alla domanda iniziale:
- «L'utente sta scrivendo». Il messaggio ha senso per un secondo e poi è stato superato dal successivo. Se un ascoltatore si perde un evento, il prossimo lo ripara. Pub/Sub.
- «Genera la fattura dell'ordine 42». Perdere l'evento significa un ordine senza fattura, scoperto a fine mese. Streams con un consumer group, un
XACKdopo che la fattura è stata scritta e un allarme sui messaggi in sospeso. - «Invalida questa cache su tutte le istanze». Un messaggio perso lascia una cache vecchia su un'istanza. Dipende da quanto dura la cache: se ha un TTL breve, Pub/Sub con quel TTL come rete di sicurezza; se vive a lungo, Streams.
Nota che la risposta non dipende da quale tecnologia «è migliore», ma dal costo dell'errore. È lo stesso ragionamento che applicherò, più avanti nella serie, al caso in cui è Redis stesso a non rispondere.
La domanda sulla persistenza
Dire che lo stream «conserva» il messaggio non vuol dire che sopravviva a un riavvio di Redis. La documentazione degli Streams precisa che uno stream, come ogni struttura, è replicato in modo asincrono e persistito su AOF e RDB, e che lo stato dei consumer group segue la stessa strada. Quanto si perde dipende allora da come hai configurato la persistenza, che è un tema a sé della serie. Finché non l'hai deciso, «Streams non perde i messaggi» è una frase più forte di quella che puoi sostenere. La stessa pagina precisa due condizioni, che qui riporto senza averle provate: per la persistenza dei messaggi serve AOF con una politica di fsync forte, e la replica asincrona non garantisce che un XADD o un cambio di stato dei gruppi arrivi alle repliche prima di un failover; il comando che riduce il rischio è WAIT, ma non lo annulla.
Nel mio backend non ci sono né l'uno né l'altro
Ho cercato nel codice del backend e del ml-service:
$ grep -rn "opsForStream\|XADD\|RedisMessageListenerContainer" backend/src/main ml-service/app
$ echo "exit=$?"
exit=1
Nessuna corrispondenza (exit 1 di grep vuol dire «niente trovato»). Il backend ha quattro file che usano SimpMessagingTemplate, cioè messaggistica STOMP su WebSocket, con un broker in memoria (enableSimpleBroker in WebSocketConfig.java, riga 64), non Redis. Quindi questo articolo non racconta un'esperienza del sito: è un confronto fra due strumenti che puoi misurare, e l'ho tenuto nella serie perché il Pub/Sub è parte della promessa della serie fin dall'inizio. Se in futuro il sito adotta Streams, questa sezione cambia.
Come scegliere
Le tre domande che mi faccio, in ordine:
- Posso perdere il messaggio se il lettore è giù? Sì: Pub/Sub è sufficiente. No: Streams.
- Un messaggio deve essere elaborato da uno solo fra molti consumatori? Sì: consumer group su uno stream. Pub/Sub consegna a tutti.
- Devo poter rileggere o rielaborare? Sì: solo uno stream lo permette.
L'ordine delle domande è quello in cui eliminano le opzioni: la prima scarta Pub/Sub se la perdita ti costa, e a quel punto le altre due servono solo a decidere come usare Streams. Se invece rispondi sì alla prima e la perdita ti costa nulla, non hai bisogno di andare avanti: il Pub/Sub è la scelta più semplice che regge il tuo caso, e la semplicità conta più della potenza che non usi.
Cosa deve fare un consumatore di Streams per essere affidabile
- Confermare con
XACKsolo dopo aver finito il lavoro, mai prima. - Poter ricevere due volte lo stesso messaggio senza danni, perché la consegna può ripetersi dopo un recupero con
XAUTOCLAIM: serve un elaboratore idempotente. - Controllare periodicamente
XPENDINGe dare un limite al numero di consegne. - Avere una politica di taglio del flusso (
MAXLEN) decisa in base a quanto resta indietro il gruppo. - Creare il gruppo con
0se deve vedere lo storico, con$solo se vuoi ignorarlo.
Le cinque regole proteggono dallo stesso scenario: il messaggio consegnato due volte o rimasto in sospeso. Conferma dopo il lavoro e idempotenza sono la coppia che rende la ripetizione innocua, XPENDING e il limite di consegne evitano che un messaggio che non si riesce a elaborare giri per sempre, mentre il taglio del flusso e il punto di partenza del gruppo riguardano lo spazio e la storia. Queste ultime sono quelle che si dimenticano quando il flusso è ancora piccolo.
Il trade-off di Streams
Ho preferito Streams a Pub/Sub quando l'evento non può andare perso, ma l'ho pagato in tre modi: devo scrivere la logica di conferma e di recupero (XACK, XAUTOCLAIM), devo decidere quando tagliare il flusso e devo monitorare i messaggi in attesa. Per un aggiornamento che il client può ricalcolare, quella complessità non si ripaga. E se ti trovi a rimpiangere un broker vero (ordinamento fra partizioni, ritenzione per tempo, ripetizione per gruppo), valuta quello strumento prima di costruirne uno sopra Streams.
Quando non usarli
- Per un lock o un'operazione che deve essere atomica: serve uno script, come in Lua in Redis.
- Per dati che devono sopravvivere con certezza: valuta prima cosa garantisce la tua configurazione di persistenza.
- Per notificare un browser: in questo backend lo fa STOMP su WebSocket con il broker in memoria di Spring, senza Redis.
Le tre esclusioni hanno la stessa radice: Streams è un registro di messaggi, non una risposta a ogni problema di coordinamento. Il lock chiede atomicità, ed è il tema dello script Lua; la durabilità dipende dalla persistenza che hai configurato e non dalla struttura che scegli; la notifica al browser ha già un canale nel sito, STOMP su WebSocket, che non passa da Redis. Se usi Streams per una di queste, stai cercando un altro strumento.
Cosa sai fare adesso
Sai dimostrare con tre comandi se un canale perde i messaggi (PUBLISH che risponde 0), sai creare un gruppo che non salta lo storico (0 e non $), e sai riconoscere e recuperare il lavoro di un consumatore morto con XPENDING e XAUTOCLAIM, tenendo d'occhio il contatore delle consegne. Per capire come Redis sceglie la rappresentazione dei dati che stai mettendo nello stream, ricomincia da strutture dati e costi; se l'evento deve anche essere non duplicato, la questione dell'idempotenza è nell'articolo sull'idempotenza dei consumatori. Per vedere cosa fa il backend quando è Redis stesso a non rispondere, c'è l'articolo su fail-open e fail-closed.
Devi decidere il meccanismo di eventi del tuo backend? Prenota una call →
Domande frequenti
Redis Pub/Sub perde i messaggi se il sottoscrittore è disconnesso?
Sì. La documentazione di Redis descrive una consegna at-most-once: il messaggio arriva una volta se arriva. In una prova con Redis 8.0.5 e 7.4.7, PUBLISH ha risposto 0 con nessun ascoltatore e il messaggio non è stato recapitato al ricollegamento.
Con Redis Streams un messaggio non confermato va perso?
No: resta nella lista dei messaggi in attesa del gruppo, visibile con XPENDING. Un altro consumatore può reclamarlo con XAUTOCLAIM dopo un tempo di inattività; il contatore delle consegne sale e va tenuto d'occhio.
Perché un gruppo creato con $ non legge i messaggi già presenti?
Perché $ significa 'parti dall'ultimo messaggio': il gruppo vede solo quelli scritti dopo. Per leggere anche lo storico si crea il gruppo con 0.
Fonti
- Redis, Pub/Sub - la semantica at-most-once della consegna, consultata il 2026-11-13.
- Redis, Streams - consumer group, lista dei messaggi in attesa, persistenza con AOF e fsync, replica asincrona e XNACK dalla 8.8.0, consultata il 2026-11-13.
- Redis, PUBLISH - il valore di ritorno è il numero di client che hanno ricevuto il messaggio, consultata il 2026-11-08.
- Redis, XADD - sintassi di
MAXLENcon=o~, consultata il 2026-11-08. - Redis, XAUTOCLAIM - sintassi con tempo minimo di inattività, disponibile dalla 6.2.0, consultata il 2026-11-13.
- Misure: i comandi dei blocchi sopra, eseguiti il 2026-11-08 su Redis 8.0.5 (nativo) e 7.4.7 (
redis/redis-stack-server:latest), e ilgrepsul codice dibackend/src/maineml-service/app.
Le fonti sono le pagine ufficiali di Redis, con la data in cui le ho lette: servono a controllare le garanzie di consegna e la sintassi, non a capire il meccanismo, che l'articolo mostra con i comandi. L'ultima voce raccoglie le misure, cioè i blocchi di codice sopra, che puoi rifare sulla tua istanza; se un numero non coincide, fidati della tua uscita e della versione che stai usando.

Commenti
Caricamento commenti...
Accedi per commentare