facebook

Blog

Resta aggiornato

Redis non è solo un key-value store ottimo per il caching ma può essere utilizzato anche come broker di messaggi: vediamo come!
“R(i)edisegnamo” il Publish/Subscribe con Redis
mercoledì 16 Settembre 2020

Recentemente ho avuto l’opportunità di lavorare per un nostro cliente con un’applicazione che faceva uso di Redis. Confrontandomi con i colleghi e studiandone la documentazione ho scoperto che anche questo strumento poteva essere utilizzato per disaccoppiare la comunicazione in un sistema software. Così, spinto dalla curiosità, ho cercato di capire meglio cosa fosse e come potevo utilizzarlo come messaging system.

Redis prende il nome da Remote Dictionary Server ed è definito come un data store in memoria. Di base, è un database del tipo key-value store, per cui la memorizzazione dei dati prevede una chiave tramite la quale si identifica un valore all’interno del database. Sono supportati diversi tipi per i valori associati alle chiavi, infatti possiamo avere, oltre le solite stringhe, anche tipi di dati strutturati come liste e insiemi.

Per sperimentare con Redis senza installarlo sulla nostra macchina, possiamo utilizzare un container Docker con il comando seguente:

docker run –name redis-pubsub -p 6379:6379 -d redis

Per scaldarci, vediamo come possiamo salvare informazioni di tipo chiave/valore utilizzando la redis-cli, uno strumento molto potente fornito a corredo. Vediamo, in particolare, i comandi SET GET per salvare e ottenere il valore salvato tramite la chiave:

L’uso più diffuso di Redis è quello come database o come cache, in particolare quando c’è necessità di accedere rapidamente a dati semplici sfruttando così la velocità della memoria centrale (è prevista comunque la possibilità di persistere i dati).

Dalla versione 2.0 però, sono stati introdotti nuovi comandi per la redis-cli che consentono di realizzare il pattern publish/subscribe. Di questo pattern ho parlato ampiamente negli articoli precedenti (ad esempio in questo) e quindi ci concentreremo su come possa essere realizzato usando Redis.

Semplicemente, è previsto che un Subscriber interessato ad un determinato topic possa sottoscriversi al relativo Channel in attesa che un Publisher ci pubblichi informazioni. I client, siano essi publisher o subscriber, si connettono a Redis tramite una connessione TCP e comunicano con il Redis Server mediante il protocollo RESP.

Redis utilizza una hash table allocata in memoria per tenere traccia dei nomi dei canali e dei relativi subscriber. Quando un nuovo subscriber si vuole sottoscrivere al canale viene fatto l’hash di quest’ultimo per individuare la lista dei suoi subscriber e il nuovo viene aggiunto a questa. Allo stesso modo, in caso di publish, viene recuperata la lista dei subscriber di un canale e per ognuno di essi sarà recapitato il messaggio.

Per mostrare un esempio di utilizzo di questa funzionalità, ho creato un semplice progetto in cui abbiamo un client (FeedRssClient) tramite il quale un utente può richiedere la sottoscrizione ad un feed Rss ed un FeedRssPublisher che riceve dal client l’URL del feed, lo elabora e ne pubblica i contenuti.

Per fare questo ho utilizzato due application console in .NET Core. Come client Redis ho utilizzato la libreria StackExchange.Redis disponibile come pacchetto Nuget.

Partiamo da FeedRssClient:

using StackExchange.Redis;
using System;
 
namespace FeedRssClient
{
    class Program
    {
        private const string RedisConnectionString = "localhost";
        private static ConnectionMultiplexer connection = ConnectionMultiplexer.Connect(RedisConnectionString);
        private const string publishChannel = "client-channel";
        private const string subscriberChannel = "rss-channel";
        private static string feedUrl = string.Empty;
 
        static void Main()
        {
            Console.WriteLine("FeedRssClient\r\n");
            Console.WriteLine($"Please enter the RSS feed that you want to follow: ");
            feedUrl = Console.ReadLine();
 
            var pubSubConnection = connection.GetSubscriber();
 
            pubSubConnection.Publish(publishChannel, $"{feedUrl}");
            var subscriberChannelMessageQueue = pubSubConnection.Subscribe(subscriberChannel);
            Console.WriteLine($"List of content: \r\n");
            subscriberChannelMessageQueue.OnMessage(message =>
            {
                Console.WriteLine(message);
            });
            Console.ReadLine();
        }
    }
}

Per prima cosa instauriamo una connessione verso il container Redis creato in precedenza tramite il metodo Connect() della ConnectionMultiplexer. Il connection multiplexer è un elemento fondamentale fornito dalla libreria StackExchange che consente di accedere al database Redis oppure di sfruttare le funzionalità di publish/subscribe.

Definiamo due channel: il primo, client-channel, sarà quello dove il FeedRssClient in questione andrà a pubblicare l’indirizzo del feed Rss a cui vuole sottoscriversi; il secondo, rss-channel, è il canale a cui il FeedRssClient si sottoscriverà.

Il metodo OnMessage() del canale di sottoscrizione fa in modo che vengano letti in maniera sequenziale i messaggi ricevuti.

Se non c’è necessità di ricevere i messaggi in ordine, è possibile utilizzare anche la versione concorrente di questo metodo, molto più performante nel caso i messaggi non abbiano correlazione tra loro.

Vediamo adesso il FeedRssPublisher:

using Microsoft.Toolkit.Parsers.Rss;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
 
namespace FeedRssPublisher
{
    class Program
    {
        private const string RedisConnectionString = "localhost";
        private static ConnectionMultiplexer connection = ConnectionMultiplexer.Connect(RedisConnectionString);
        private const string publishChannel = "rss-channel";
        private const string subscriberChannel = "client-channel";
 
        static void Main()
        {
            Console.WriteLine("FeedRssPublisher\r\n");
            var feed = new Feed();
            var pubSubConnection = connection.GetSubscriber();
            var feedUrl = string.Empty;
            var subscriberChannelMessageQueue = pubSubConnection.Subscribe(subscriberChannel);
 
            subscriberChannelMessageQueue.OnMessage(async message =>
            {
                feedUrl = message.ToString().Remove(0, subscriberChannel.Length + 1);
                var rss = await feed.ParseRSSAsync(feedUrl);
                Console.WriteLine($"Feed Received: {feedUrl}\r\n");
                if (rss != null)
                {
                    Console.WriteLine("Start publishing contents ...");
                    foreach (var item in rss)
                    {
                        pubSubConnection.Publish(publishChannel, $"{item.Title}" + $"\r\n{item.Summary}" + $"\r\n{item.FeedUrl}\r\n");
                    }
                }
            });
 
            Console.ReadLine();
 
        }
 
        class Feed
        {
            public async Task<IEnumerable<RssSchema>> ParseRSSAsync(string feed)
            {
                IEnumerable<RssSchema> rss = null;
 
                using (var client = new HttpClient())
                {
                    try
                    {
                        feed = await client.GetStringAsync(feed);
                    }
                    catch (Exception)
                    {
                        throw;
                    }
                }
 
                if (feed != null)
                {
                    var parser = new RssParser();
                    rss = parser.Parse(feed);
                }
 
                return rss;
            }
        }
    }
}

Anche in questo caso, creiamo una connessione verso il container Redis e definiamo i due channel che, in questo caso, avranno i ruoli invertiti rispetto al FeedRssClient.

Infatti, il FeedRssPublisher pubblicherà i messaggi sul channel rss-channel mentre si sottoscriverà al channel client-channel.

Subito dopo la sottoscrizione all’interno del metodo OnMessage() vengono estratti i feed RSS mediante l’utilizzo della classe Feed e successivamente per ogni feed andiamo a pubblicarne il titolo, il summary e l’URL.

Proviamo i due client con il feed RSS del nostro blog https://www.blexin.com/rssfeed/articles?lang=en-US:

Il publisher riceve l’URL del feed RSS e inizia a pubblicarne i contenuti:

Il client riceve la lista dei contenuti: 

Possiamo naturalmente utilizzare anche la redis-cli per la sottoscrizione al channel rss-channel:

Come abbiamo visto dall’esempio è molto semplice realizzare il pattern publish/subscribe con Redis ed ottenere un sistema software disaccoppiato.

L’uso di Redis consente di ottenere un sistema di messaggistica snello e performante sia per la natura in-memory di Redis che per la scalabilità ovvero la possibilità di configurarlo in maniera distribuita.
Ad esempio, il meccanismo di Pub/Sub è molto utilizzato nell’ambito delle applicazioni real-time. Si pensi, ad esempio, alle chat room o a quegli scenari applicativi in cui bisogna convogliare una grande mole di dati (data ingestion) e processare questi grossi volumi a grande velocità (stream processing). Un altro esempio interessante, che vedremo in un prossimo articolo, è la possibilità di usare Redis per scalare applicazioni ASP.NET Core SignalR.

Tuttavia, c’è da considerare che questo meccanismo appena visto è di tipo fire & forget e non prevede quindi nessuna persistenza per i messaggi con conseguente impossibilità di recuperarli in caso di disconnessione di un client.

Spero che questa funzionalità di Redis vi possa essere utile.

Vi lascio il link del repository GitHub dove potete trovare il codice utilizzato per l’esempio e i riferimenti utilizzati:

https://redis.io/topics/pubsub

https://making.pusher.com/redis-pubsub-under-the-hood

Al prossimo articolo!