Vediamo come poter implementare un semplice sistema di caching thread-safe in memoria.

Partiremo dalla classe CuncurrentDictionary introdotta da Microsoft con il framework 4.0, che implementa una raccolta di coppie chiave/valore a cui si può accedere simultaneamente da più thread. (rif : Msdn).

Iniziamo con creare la nostra classe : 

namespace DotNetCode.DarthFabio
{
public class InProcessCachingDictionary
{
private static ConcurrentDictionary<string, object> InternalCollection =
new ConcurrentDictionary<string, object>();
}
}

Implementiamo i nostri primi metodi :

  • bool ContainsKey(string key) : metodo che prende in input una chiave del nostro dictionary e ci restituisce true se è presente un elemento con la chiave specificata,in caso contrario ci restituisce false
  • object GetItem(string key) : metodo che restituisce il valore presente nel dictionary per la chiave specificata.
namespace DotNetCode.DarthFabio
{
public class InProcCachingDictionary
{
private static readonly ConcurrentDictionary<string, object> InternalCollection =
new ConcurrentDictionary<string, object>();

public static bool ContainsKey(string key)
{
return InternalCollection.ContainsKey(key);
}

public static object GetItem(string key)
{
return InternalCollection[key];
}
}

A questo punto mi rendo conto che potreste chiedervi il perchè stiate ancora perdendo tempo a leggere questo post…

Cerchiamo di rendere più utile questa classe e modifichiamo il metodo GetItem introducendo i generics (rif: Msdn)

public static T GetItem<T>(string key)
{
return (T)InternalCollection[key];
}
In questo modo possiamo castare direttamente l’elemento restituito dal nostro dictionary al tipo che ci aspettiamo.
Esempio :
 
...
var myCachedItem = InProcCachingDictionary.GetItem<string>("MyBancomatPin");
...
Il metodo così implementato ci espone a tre possibili eccezioni :
  • ArgumentNullException : se la key è null.
  • KeyNotFoundException : se l’elemento non è presente nel dictionary.
  • InvalidCastException : se l’elemento presente nel dictionary non è del tipo richiesto.

Potremmo trovarci in uno scenario in cui può bastare che il nostro metodo GetItem ci restuisca sempre e comunque un valore indipendentemente dalla validità dei parametri inseriti.

Implementiamo quindi il metodo  SafeGetItem<T> che in caso di una delle tre eccezioni precedenti ci restituirà il valore di default per il tipo T richiesto.

public static T SafeGetItem<T>(string key)
{
object myCachedItem;
if (!InternalCollection.TryGetValue(key, out myCachedItem))
return default(T);

return myCachedItem.GetType() != typeof (T) ? default(T) : (T) InternalCollection[key];
}
Ora possiamo recuperare dei valori dal nostro dizionario, ma manca l’opportuno metodo per inserirli.
 
public static void AddOrUpdate(string key, object value)
{
InternalCollection.AddOrUpdate(key, value, (k, oldValue) => value);
}
Il nostro metodo AddOrUpdate non è altro che un ”passacarte“ verso l’omonimo metodo del ConcurrentDictionary,
[ ConcurrentDictionary.AddOrUpdate (TKey, TValue, Func<TKey, TValue, TValue>) :  Aggiunge una coppia chiave/valore a ConcurrentDictionary<TKey, TValue>, se la chiave non esiste già, oppure aggiorna una coppia chiave/valore in ConcurrentDictionary<TKey, TValue> utilizzando la funzione specificata, se la chiave esiste già.(rif: Msdn) ], che si limita ad aggiungere un valore al dictionary se non esiste, in caso invece esista lo sovrascrive.
 
Caliamo tutto questo in un possibile scenario d’uso, in cui vogliamo accedere a un elemento della nostra cache “myKey” di tipo stringa e in caso non sia presente sappiamo che il valore è reperibile nel file di configurazione del nostro applicativo.
 
string myStringCachedItem;
try
{
if (InProcCacheRepository.ContainsKey("myKey"))
myStringCachedItem = InProcCacheRepository.GetItem<string>("myKey");
else
{
var myItemToCache = System.Configuration.ConfigurationManager.AppSettings["myKey"];
InProcCacheRepository.AddOrUpdate("myKey", myItemToCache);

myItemToCache = myItemToCache;
}
}
catch (Exception)
{
//Gestiamo le eccezioni descritte precedentemente.
throw;
}
Il tutto è sia poco elegante che di scarsa utilità messo in questa forma. Potremmo eliminare il check sull’esistenza del nostro elemento in cache pre-popolando il nostro dictionary sull’ Init() della nostra applicazione; in modo da essere sicuri che la verifica “InProcCacheRepository.ContainsKey("myKey"))” sia sempre true.

 

In questo caso il nostro codice si limiterebbe a :

var myStringCachedItem = InProcCacheRepository.GetItem<string>("myKey");
Anche questo approccio ha più aspetti negativi che positivi:
  • innanzitutto potremmo essere in un caso in cui i valori di configurazione da caricare sull’ Init() siano molti e che non vengano nemmeno usati tutti.
  • In questo scenario non si vede nemmeno perché dovremmo usare il nostro dictionary e non accedere direttamente al nostro file di configurazione.

Introduciamo a questo punto un nuovo metodo che ci permette di rendere il nostro codice più snello e robusto.

public static T GetOrAddItem<T>(string key, T value)
{
object outValue;
if (InternalCollection.TryGetValue(key, out outValue)) return (T)outValue;

AddOrUpdate(key, value);

return value;
}

Questo codice ci permette di scrivere il codice precedente nella seguente maniera :
 
var myStringCachedItem = InProcCacheRepository.GetOrAddItem(“myKey”,System.Configuration.ConfigurationManager.AppSettings["myKey"]);

In questo modo siamo anche sicuri di non avere le seguenti eccezioni :

  • KeyNotFoundException : in quanto se la chiave non è presente la creiamo.
  • InvalidCastException : siamo noi a passare sul metodo il tipo e quindi siamo sicuri che il tipo restituito sia lo stesso, sarebbe il compilatore stesso a segnalarci eventuali conflitti di tipo.

 

Consideriamo ora che nel nostro web.config abbiamo le seguenti chiavi di configurazione:

  • una lista di estensioni di files per cui è permesso fare l’upload
    <add key="AllowedMimeTypes" value="txt|doc|docx|pdf"/>
  • il valore di timeout di esecuzione di un operazione
    <add key="MyBatchOp.Timeout" value="5000"/>
  • una stringa di connessione al nostro database criptata
    <add key="MyDb.ConnectionString" value="xxxTHSHSUSNSUjMSSIKSMmsjdidmmdDKKDMdmdjdidmmd" />

Vediamo come si accederebbe a questi valori:

var MyList = Configuration.ConfigurationManager.AppSettings["AllowedMimeTypes”].Split('|').ToList();

var myTimeOut = long.parse(Configuration.ConfigurationManager.AppSettings["MyBatchOp.Timeout”]);

var MyConnString = MySecurityManager.Decrypt(Configuration.ConfigurationManager.AppSettings["MyDb.ConnectionString”]);
Quindi ad ogni request che impatta moduli della nostra applicazione in cui accediamo a questi valori di cache avremmo l’overhead dovuto alle operazioni di Split(),cast e decriptaggio.
 
A questo usando il nostro dictionary possiamo salvare in cache direttamente i valori effettivi di uso e risparmiare su questo overhead.
 
Esempio :
 
string MyList;
try
{
if (InProcCacheRepository.ContainsKey("AllowedMimeTypes"))
myStringCachedItem = InProcCacheRepository.GetItem<IList<string>>("AllowedMimeTypes");
else
{
var myItemToCache = Configuration.ConfigurationManager.AppSettings["AllowedMimeTypes”].Split('|').ToList();
InProcCacheRepository.AddOrUpdate("myKey", myItemToCache);

myItemToCache = myItemToCache;
}
}
catch (Exception)
{
//Gestiamo le eccezioni descritte precedentemente.
throw;
}
Usiamo ora il metodo GetOrAddItem :
 
var myStringCachedItem = InProcCacheRepository.GetOrAddItem(“MyList”,Configuration.ConfigurationManager.AppSettings["AllowedMimeTypes”].Split('|').ToList());

Il che rende il tutto probabilmente più elegante (de gustibus) ma abbastanza inutile, in quanto non risolveremmo il problema di overhead per lo Split() in quanto anche sa la chiave è presente nel nostro dictionary l’operazione di Split() viene effettuata lo stesso nel momento in cui viene chiamato il metodo.
 
A questo punto ci vengono in aiuto i delegati e in questo caso il delegato Func<T, TResult> (ref : Msdn).
 
Andiamo a implementare un overload del metodo GetOrAddItem.
 
public static T GetOrAddItem<T>(string key, Func<T> value)
{
object outValue;
if (InternalCollection.TryGetValue(key, out outValue)) return (T)outValue;

var valueT = value.Invoke();

AddOrUpdate(key, valueT);

return valueT;
}

A questo punto il codice diventa :
 
var myStringCachedItem = InProcCacheRepository.GetOrAddItem("MyList",
() => System.Configuration.ConfigurationManager.AppSettings["AllowedMimeTypes"].Split('|').ToList());

In questa forma siamo sicuri che la seguente istruzione “System.Configuration.ConfigurationManager.AppSettings["AllowedMimeTypes"].Split('|').ToList());” viene elaborata sono all’interno del metodo e solo in caso la chiave non esista.
 
La nostra classe diventa a questo punto :
 
public class InProcCacheRepository
{
private static readonly ConcurrentDictionary<string, object> InternalCollection =
new ConcurrentDictionary<string, object>();

public static bool ContainsKey(string key)
{
return InternalCollection.ContainsKey(key);
}

public static T GetItem<T>(string key)
{
return (T)InternalCollection[key];
}

public static T SafeGetItem<T>(string key)
{
object myCachedItem;
if (!InternalCollection.TryGetValue(key, out myCachedItem))
return default(T);

return myCachedItem.GetType() != typeof (T) ? default(T) : (T) InternalCollection[key];
}

public static T GetOrAddItem<T>(string key, Func<T> value)
{
object outValue;
if (InternalCollection.TryGetValue(key, out outValue)) return (T)outValue;

var valueT = value.Invoke();

AddOrUpdate(key, valueT);

return valueT;
}

public static T GetOrAddItem<T>(string key, T value)
{
object outValue;
if (InternalCollection.TryGetValue(key, out outValue)) return (T)outValue;

AddOrUpdate(key, value);

return value;
}

public static void Clear()
{
InternalCollection.Clear();
}

public static void AddOrUpdate(string key, object value)
{
InternalCollection.AddOrUpdate(key, value, (k, oldValue) => value);
}
}
 
L’aver usato un CuncurrentDictionary come repository delle mie coppie chiavi valori mi garantisce la sincronizzazione dello stesso rispetto a chiamate concorrenti (scenario tipico in una web application) e che l’inizializzazione e popolamento dello stesso siano thread-safe.
 
L’utilizzo dei delegati mi permette di ottimizzare l’esecuzione del codice e di poter utilizzare operazioni complesse per la creazione dei miei oggetti da salvare in cache, operazioni che vengono effettuate solamente in fase di recupero del valore per la prima volta e non impattano il normale accesso alla cache.

Autore:


blog comments powered by Disqus

 

Calendar

<<  dicembre 2017  >>
lunmarmergiovensabdom
27282930123
45678910
11121314151617
18192021222324
25262728293031
1234567

Vedi i post nel calendario più grande

Category list