facebook

Blog

Resta aggiornato

Vediamo come gestire il caching in scenari distribuiti con .NET Core e come scegliere il giusto repository dei dati
Cache distribuita in Asp.NET Core
mercoledì 25 Settembre 2019

Nel precedente articolo vi ho parlato di come gestire il caching in un’applicazione Asp.Net Core utilizzando in-memory caching. L’utilizzo di questo tipo di caching funziona quando la vostra applicazione è ospitata su un singolo server. Quali sono, invece, gli strumenti che ci mette a disposizione il framework .NET Core per fare caching in uno scenario distribuito, come per esempio in cloud?

Interfaccia IDistributedCache

Il framework ci mette a disposizione questa interfaccia:

public interface IDistributedCache
{
    byte[] Get(string key);
    Task <byte[]> GetAsync(string key);
 
    void Set(string key, byte[] value, DistributedCacheEntryOptions options);
    Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options);
 
    void Refresh(string key);
    Task RefreshAsync(string key);
 
    void Remove(string key);
    Task RemoveAsync(string key);
}

Se ricordate quella utilizzata nel precedente articolo (IMemoryCache), potrete notare somiglianze ma anche tante differenze:

  1. Metodi async
  2. Metodi di Refresh (Per aggiornare le chiavi di cache senza richiedere dati)
  3. Metodi byte based e non object based

Al momento, sono presenti 3 implementazioni fornite da Microsoft: una in memory che viene utilizzata principalmente nella fase di sviluppo di un’applicazione, una basata su SQL server e l’ultima basata su REDIS.
Per utilizzare quella basata su REDIS è necessaria l’installazione del pacchetto NuGet Microsoft.Extensions.Caching.Redis.

Distributed Memory Cache

Questa implementazione è fornita direttamente dal framework e conserva i nostri dati in memoria. Non è propriamente un tipo di cache distribuita, perché i dati vengono salvati dalla istanza dell’app sul server su cui gira. Può essere comunque utile in alcuni scenari:

  • Sviluppo e testing
  • Quando un singolo server è usato in produzione senza problemi sull’utilizzo della memoria.

In ogni caso, abilitiamo la nostra applicazione ad utilizzare una “vera” soluzione distribuita quando diventa necessaria (l’interfaccia utilizzata per il salvataggio e il caricamento dei dati è la stessa).
Per abilitare questa implementazione di IDistributedCache, basta registrarla all’interno della classe Startup in questo modo:

services.AddDistributedMemoryCache();

Distributed SQL Server Cache

Questa implementazione abilita la cache distribuita ad usare un database SQL Server come storage. Ci sono dei passi di configurazione da effettuare. Il primo consiste nel creare le tabelle che verranno usate per persistere i dati.
Per rendere semplice questa operazione è presente un tool a linea di comando, che, tramite una semplice istruzione

dotnet sql-cache create <connection string > <schema > <table >

permette di creare una tabella con questa struttura:

CREATE TABLE [dbo].[CacheTable](
    [Id] [nvarchar](449) NOT NULL,
    [Value] [varbinary](max) NOT NULL,
    [ExpiresAtTime] [datetimeoffset](7) NOT NULL,
    [SlidingExpirationInSeconds] [bigint] NULL,
    [AbsoluteExpiration] [datetimeoffset](7) NULL,
 CONSTRAINT [pk_Id] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, 
    IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, 
    ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
 
CREATE NONCLUSTERED INDEX [Index_ExpiresAtTime] ON [dbo].[CacheTable]
(
    [ExpiresAtTime] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, 
    SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, 
    ONLINE = OFF, ALLOW_ROW_LOCKS = ON, 
    ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

Il secondo ed ultimo passo nella configurazione di questa implementazione consiste nel registrarla nella nostra applicazione. Nella classe di Startup, all’interno del metodo ConfigureServices, andiamo ad aggiungere questo blocco di codice:

services.AddDistributedSqlServerCache(o =>
{
    o.ConnectionString = Configuration["ConnectionStrings:Default"];
    o.SchemaName = "dbo";
    o.TableName = "Cache";
});

Distributed Redis Cache

L’utilizzo di Redis è molto diffuso, quando si parla di scenari di caching distribuito. Redis è un datastore in memoria veloce, open source e di tipo chiave valore. Fornisce tempi di risposta inferiori al millisecondo, consentendo milioni di richieste al secondo per ogni applicazione in tempo reale in svariati campi.
Se l’infrastruttura della vostra applicazione è basata sul cloud di Azure, è disponibile il servizio Azure Redis Cache, configurabile dalla vostra sottoscrizione.

Per poter utilizzare la cache distribuita su Redis, è necessaria l’installazione del package Microsoft.Extensions.Caching.Redis.
Il primo passo è quello di configurare il servizio nella classe Startup:

public void ConfigureServices(IServiceCollection services)
{
  // Add framework services.
  // ... altri servizi ...
   
  services.AddDistributedRedisCache(cfg => 
  {
    cfg.Configuration = Configuration.GetConnectionString("redis");
  });
}

Se stiamo utilizzando Redis Cache di Azure, possiamo reperire la connection string accedendo alla sezione “Chiavi di accesso” del pannello.

Utilizzo di IDistributedCache

Utilizzare l’interfaccia IDistributedCache è estremamente semplice: se avete già letto il mio articolo sull’utilizzo dell’interfaccia IMemoryCache, troverete parecchi punti in comune. Volendola utilizzare in un controller, per prima cosa è necessario iniettarne l’istanza nel costruttore, come potete vedere dal codice che segue:

public class HomeController : Controller
{
    private readonly IDistributedCache _cache;
 
    public HomeController(IDistributedCache cache)
    {
        _cache = cache;
    }
    public async Task <IActionResult > Index()
    {
        await _cache.SetStringAsync("TestString", "TestValue");
 
        var value = _cache.GetString("TestString");
 
        return View();
    }
}

Se eseguite questo codice e inserite un breakpoint sull’ultima riga del metodo Index, vedrete questo risultato:

Possiamo facilmente controllare la durata della cache utilizzando la classe DistributedCacheEntryOptions. Nel codice che segue, ne creiamo un’istanza, impostando la durata ad un’ora:

public async Task <IActionResult > Index()
{
    var options = new DistributedCacheEntryOptions
    {
        AbsoluteExpiration = DateTime.Now.AddHours(1)
    };
 
    await _cache.SetStringAsync("TestString", "TestValue", options);
 
    var value = _cache.GetString("TestString");
 
    return View();
}

Conclusioni e raccomandazioni

La decisione su quale implementazione di IDistributedCache utilizzare nelle nostre app varia in base a diversi fattori. La scelta tra Redis e SQL (escludo volutamente l’implementazione memory, non utilizzabile se non a fini di sviluppo e testing) dovrà essere fatta in base all’infrastruttura a vostra disposizione, ai requisiti di prestazione a cui dovete sottostare e all’esperienza del vostro team di sviluppo. Se il team si sente a proprio agio a lavorare con Redis, questa sicuramente potrebbe essere la scelta migliore. L’implementazione SQL è comunque una buona soluzione ma va ricordato che il recupero di dati non offrirà prestazioni eccellenti, quindi i dati da cachare vanno scelti in modo oculato.