Autore: Iqbal Khan
Una cache distribuita consente di migliorare notevolmente le prestazioni e la scalabilità delle applicazioni. Le prestazioni dell'applicazione sono migliorate perché una cache in memoria è molto più veloce per l'accesso ai dati rispetto a un database. Inoltre, la scalabilità si ottiene espandendo la cache su più server come cache distribuita e guadagnando non solo più capacità di archiviazione, ma anche più transazioni al secondo.
Nonostante tali potenti vantaggi, molte cache in memoria devono affrontare un problema. E questo ha a che fare con il fatto che la maggior parte dei dati è relazionale mentre una cache è solitamente una semplice tabella hash con un concetto di coppia chiave-valore. Ogni elemento viene archiviato nella cache in modo indipendente senza alcuna conoscenza di altri elementi correlati. Inoltre, ciò rende difficile per le applicazioni tenere traccia delle relazioni tra diversi elementi memorizzati nella cache sia per recuperarli che per l'integrità dei dati nel caso in cui un elemento venga aggiornato o rimosso e anche i relativi elementi vengano aggiornati o rimossi nel database. Quando ciò accade, la cache non lo sa e non può gestirlo.
Una tipica applicazione reale si occupa di dati relazionali che hanno relazioni uno-a-uno, molti-a-uno, uno-a-molti e molti-a-molti con altri elementi di dati nel database. Ciò richiede il mantenimento dell'integrità referenziale tra i diversi elementi di dati correlati. Pertanto, al fine di preservare l'integrità dei dati nella cache, la cache deve comprendere queste relazioni e mantenere la stessa integrità referenziale.
Per gestire queste situazioni, la dipendenza dalla cache è stata introdotta da Microsoft in ASP.NET Cache. La dipendenza dalla cache ti consente di mettere in relazione vari elementi memorizzati nella cache e quindi ogni volta che aggiorni o rimuovi qualsiasi elemento memorizzato nella cache, la cache rimuove automaticamente tutti i relativi elementi memorizzati nella cache per garantire l'integrità dei dati. Quindi, quando l'applicazione non trova questi elementi correlati nella cache la prossima volta che ne ha bisogno, l'applicazione va al database e recupera la copia più recente di questi elementi, quindi li memorizza nuovamente nella cache mantenendo l'integrità referenziale corretta.
Questa è un'ottima funzionalità di ASP.NET Cache, ma ASP.NET Cache è di progettazione una cache autonoma che è utile solo per ambienti in-process a server singolo. Ma, per la scalabilità, è necessario utilizzare a cache distribuita che può vivere al di fuori del processo della tua applicazione e può scalare su più server cache. NCache è una tale cache e fortunatamente fornisce la stessa funzione di dipendenza dalla cache in un ambiente distribuito. È possibile avere elementi memorizzati nella cache in un server di cache fisico a seconda degli elementi memorizzati nella cache in un altro server di cache fisico, purché facciano entrambi parte della stessa cache logica in cluster. E, NCache si occupa di tutti i problemi di integrità dei dati sopra menzionati.
Questo articolo spiega come utilizzare la dipendenza dalla cache per gestire le relazioni uno-a-uno, uno-molti e molti-a-molti nella cache. Utilizza NCache come esempio, ma gli stessi concetti si applicano ad ASP.NET Cache.
Sebbene, NCache fornisce vari tipi di dipendenze tra cui Dipendenza dai dati, Dipendenza da file, Dipendenza SQLe Dipendenza personalizzata, in questo articolo viene illustrata solo la dipendenza dai dati per la gestione delle relazioni tra gli elementi memorizzati nella cache.
La dipendenza dai dati è una funzionalità che consente di specificare che un elemento memorizzato nella cache dipende da un altro elemento memorizzato nella cache. Quindi, se il secondo elemento memorizzato nella cache viene aggiornato o rimosso, anche il primo elemento che dipendeva da esso viene rimosso dalla cache. Dipendenza dati consente di specificare dipendenze multilivello in cui A dipende da B, che quindi dipende da C. Quindi, se C viene aggiornato o rimosso, sia A che B vengono rimossi dalla cache.
Di seguito è riportato un breve esempio di come utilizzare la dipendenza dai dati per specificare la dipendenza a più livelli.
public static void CreateDependencies(ICache _cache)
{
try
{
string keyC = "objectC-1000";
Object objC = new Object();
string keyB = "objectB-1000";
Object objB = new Object();
string keyA = "objectA-1000";
Object objA = new Object();
// Initializing cacheItems
var itemOne = new CacheItem(objA);
var itemTwo = new CacheItem(objB);
var itemThree = new CacheItem(objC);
// Adding objA dependent on ObjB
itemOne.Dependency = new KeyDependency(keyB);
itemTwo.Dependency = new KeyDependency(keyC);
//Adding items to cache
_cache.Add(keyC, itemThree);
_cache.Add(keyB, itemTwo);
_cache.Add(keyA, itemOne);
// Removing "objC" automatically removes “objB” as well as "ObjA"
_cache.Remove(keyC);
_cache.Dispose();
}
catch (Exception e)
{
throw;
}
}
L'esempio seguente viene utilizzato in questo articolo per dimostrare come vengono gestiti i vari tipi di relazioni nella cache.
Nel diagramma sopra, sono mostrate le seguenti relazioni:
Per le relazioni precedenti, vengono progettati i seguenti oggetti di dominio.
class Customer
{
public string CustomerID;
public string CompanyName;
public string ContactName;
public string ContactTitle;
public string Phone;
public string Country;
public IList<Order> _OrderList;
}
class Product
{
public int ProductID;
public string ProductName;
public Decimal UnitPrice;
public int UnitsInStock;
public int UnitsOnOrder;
public int ReorderLevel;
public IList<Order> _OrderList;
}
class Order
{
public int OrderId;
public string CustomerID;
public DateTime OrderDate;
public DateTime RequiredDate;
public DateTime ShippedDate;
public int ProductID;
public Decimal UnitPrice;
public int Quantity;
public Single Discount;
public Customer _Customer;
public Product _Product;
}
Come puoi vedere, le classi Cliente e Prodotto contengono un _Lista degli ordini per contenere un elenco di tutti gli oggetti Order correlati a questo cliente. Allo stesso modo, la classe Order contiene _Cliente ed _Prodotto membri dei dati per puntare al relativo oggetto Cliente o Prodotto. Ora, è compito del codice di persistenza caricare questi oggetti dal database per garantire che ogni volta che un Cliente viene caricato, vengano caricati anche tutti i suoi oggetti Ordine.
Di seguito, mostrerò come ciascuna di queste relazioni viene gestita nella cache.
Ogni volta che hai recuperato un oggetto dalla cache che ha anche una relazione uno-a-uno o molti-a-uno con un altro oggetto, il tuo codice di persistenza potrebbe aver caricato anche l'oggetto correlato. Tuttavia, non è sempre necessario caricare l'oggetto correlato perché l'applicazione potrebbe non averne bisogno in quel momento. Se il tuo codice di persistenza ha caricato l'oggetto correlato, devi gestirlo.
Ci sono due modi per gestirlo. Chiamerò un modo ottimista e l'altro pessimista e spiegherò ciascuno di essi di seguito:
Di seguito è riportato il codice sorgente per gestire la situazione ottimistica. Si noti che sia l'oggetto primario che entrambi i relativi oggetti vengono memorizzati nella cache come un unico elemento perché la serializzazione dell'oggetto primario includerebbe anche gli oggetti correlati.
static void Main(string[] args)
{
string cacheName = "myReplicatedCache";
ICache _cache = CacheManager.GetCache(cacheName);
OrderFactory oFactory = new OrderFactory();
Order order = new Order();
order.OrderId = 1000;
oFactory.LoadFromDb(order);
Customer cust = order._Customer;
Product prod = order._Product;
var itemOne = new CacheItem(order);
// please note that Order object serialization will
// also include Customer and Product objects
_cache.Add(order.OrderId.ToString(), itemOne);
_cache.Dispose();
}
Di seguito è riportato il codice sorgente per gestire la situazione pessimistica poiché lo scenario ottimistico non richiede l'uso della dipendenza dai dati.
static void Main(string[] args)
{
string cacheName = "myReplicatedCache";
ICache _cache = CacheManager.GetCache(cacheName);
OrderFactory oFactory = new OrderFactory();
Order order = new Order();
order.OrderId = 1000;
oFactory.LoadFromDb(order);
Customer cust = order._Customer;
Product prod = order._Product;
string custKey = "Customer:CustomerID:" + cust.CustomerID;
_cache.Insert(custKey, cust);
string prodKey = "Product:ProductID:" + prod.ProductID;
_cache.Insert(prodKey, prod);
string[] depKeys = { prodKey, custKey };
string orderKey = "Order:OrderID:" + order.OrderId;
// We are setting _Customer and _Product to null so they
// don't get serialized with Order object
order._Customer = null;
order._Product = null;
var item = new CacheItem(order);
item.Dependency = new CacheDependency(null, depKeys);
_cache.Add(orderKey, item);
_cache.Dispose();
}
Il codice precedente carica un oggetto Order dal database e gli oggetti Customer e Product vengono caricati automaticamente con esso perché l'oggetto Order ha una relazione molti-a-uno con essi. L'applicazione aggiunge quindi gli oggetti Cliente e Prodotto alla cache e quindi aggiunge l'oggetto Ordine alla cache, ma con una dipendenza sia dagli oggetti Cliente che Prodotto. In questo modo, se uno di questi oggetti Cliente o Prodotto viene aggiornato o rimosso nella cache, l'oggetto Ordine viene automaticamente rimosso dalla cache per preservare l'integrità dei dati. L'applicazione non deve tenere traccia di questa relazione.
Ogni volta che hai recuperato un oggetto dalla cache che ha anche una relazione uno-a-molti con un altro oggetto, il tuo codice di persistenza può caricare sia l'oggetto primario che una raccolta di tutti i suoi oggetti correlati uno-a-molti. Tuttavia, non è sempre necessario caricare gli oggetti correlati perché l'applicazione potrebbe non averne bisogno in questo momento. Se il tuo codice di persistenza ha caricato gli oggetti correlati, devi gestirli nella cache. Si noti che gli oggetti correlati sono tutti conservati in un'unica raccolta e questo introduce problemi propri che vengono discussi di seguito.
Ci sono tre modi per gestirlo. Chiamerò un modo ottimista, uno leggermente pessimista e uno veramente pessimista e spiegherò ciascuno di essi di seguito:
Di seguito è riportato un esempio di come gestire ottimisticamente le relazioni uno-a-molti. Si noti che la raccolta contenente gli oggetti correlati viene serializzata come parte dell'oggetto primario quando viene inserita nella cache.
static void Main(string[] args)
{
string cacheName = "ltq";
ICache _cache = CacheManager.GetCache(cacheName);
CustomerFactory cFactory = new CustomerFactory();
Customer cust = new Customer();
cust.CustomerID = "ALFKI";
cFactory.LoadFromDb(cust);
// please note that _OrderList will automatically get
// serialized along with the Customer object
string custKey = "Customer:CustomerID:" + cust.CustomerID;
_cache.Add(custKey, cust);
_cache.Dispose();
}
Di seguito è riportato un esempio di come gestire la relazione uno-a-molti in modo leggermente pessimistico.
static void Main(string[] args)
{
string cacheName = "myReplicatedCache";
ICache _cache = CacheManager.GetCache(cacheName);
CustomerFactory cFactory = new CustomerFactory();
Customer cust = new Customer();
cust.CustomerID = "ALFKI";
cFactory.LoadFromDb(cust);
IList<Order> orderList = cust._OrderList;
// please note that _OrderList will not be get
// serialized along with the Customer object
cust._OrderList = null;
string custKey = "Customer:CustomerID:" + cust.CustomerID;
var custItem = new CacheItem(cust);
_cache.Add(custKey, custItem);
// let's reset the _OrderList back
cust._OrderList = orderList;
string[] depKeys = { custKey };
string orderListKey = "Customer:OrderList:CustomerId" + cust.CustomerID;
IDictionary<string, CacheItem> dictionary = new Dictionary<string, CacheItem>();
foreach (var order in orderList)
{
var orderItem = new CacheItem(order);
orderItem.Dependency = new CacheDependency(null, depKeys);
dictionary.Add(orderListKey, orderItem);
}
_cache.AddBulk(dictionary);
_cache.Dispose();
}
Nell'esempio precedente, l'elenco degli oggetti Order correlati a questo viene memorizzato nella cache separatamente. L'intera raccolta viene memorizzata nella cache come un unico elemento perché si presume che nessuno modificherà direttamente i singoli oggetti Order separatamente. L'applicazione lo recupererà sempre tramite questo cliente e modificherà e ricollegherà nuovamente l'intera raccolta.
Un altro caso è la gestione pessimistica delle relazioni uno-a-molti, che è simile al modo in cui gestiamo le raccolte nella cache. Questo argomento è discusso nella sezione successiva.
Esistono molte situazioni in cui si recupera una raccolta di oggetti dal database. Ciò potrebbe essere dovuto a una query eseguita o potrebbe essere una relazione uno-a-molti che restituisce una raccolta di oggetti correlati sul lato "molti". In ogni caso, ciò che ottieni è una raccolta di oggetti che devono essere gestiti nella cache in modo appropriato.
Esistono due modi per gestire le raccolte come spiegato di seguito:
Di seguito è riportato un esempio di come gestire le raccolte in modo ottimistico.
static void Main(string[] args)
{
string cacheName = "myReplicatedCache";
ICache _cache = CacheManager.GetCache(cacheName);
CustomerFactory cFactory = new CustomerFactory();
Customer cust = new Customer();
string custListKey = "CustomerList:LoadByCountry:Country:United States";
IList<Customer> custList = cFactory.LoadByCountry("United States");
IDistributedList<Customer> list = _cache.DataTypeManager.CreateList<Customer>(custListKey);
// please note that all Customer objects kept in custList
// will be serialized along with the custList
foreach (var customer in custList)
{
// Add products to list
list.Add(customer);
}
_cache.Dispose();
}
Nell'esempio precedente, l'intera raccolta viene memorizzata nella cache come un unico articolo e tutti gli oggetti Cliente conservati all'interno della raccolta vengono serializzati automaticamente insieme alla raccolta e alla cache. Pertanto, non è necessario creare alcuna dipendenza dai dati qui.
Di seguito è riportato un esempio di come gestire le raccolte in modo pessimistico.
static void Main(string[] args)
{
string cacheName = "myReplicatedCache";
ICache _cache = CacheManager.GetCache(cacheName);
CustomerFactory cFactory = new CustomerFactory();
Customer cust = new Customer();
IList<Customer> custList = cFactory.LoadByCountry("United States");
ArrayList custKeys = new ArrayList();
// Let's cache individual Customer objects and also build
// an array of keys to be used later in CacheDependency
foreach (Customer c in custList)
{
string custKey = "Customer:CustomerID:" + c.CustomerID;
custKeys.Add(custKey);
_cache.Insert(custKey, c);
}
string custListKey = "CustomerList:LoadByCountry:Country:United States";
// please note that this collection has a dependency on all
// objects in it separately. So, if any of them are updated or
// removed, this collection will also be removed from cache
IDistributedList<Customer> list = _cache.DataTypeManager.CreateList<Customer>(custListKey);
foreach (var customer in custList)
{
// Add products to list
var item = new CacheItem(customer);
item.Dependency = new CacheDependency(null, (string[])custKeys.ToArray());
list.Add(customer);
}
_cache.Dispose();
}
Nell'esempio precedente, ogni oggetto della raccolta viene memorizzato nella cache come un elemento separato e quindi l'intera raccolta viene memorizzata nella cache insieme a un elemento. La raccolta ha una dipendenza dai dati su tutti i suoi oggetti che sono memorizzati nella cache separatamente. In questo modo, se uno di questi oggetti viene aggiornato o rimosso, anche la raccolta viene rimossa dalla cache.
Autore: Iqbal Khan lavori per Alachisoft , azienda di software leader nella fornitura di soluzioni di caching distribuito .NET e Java, mappatura O/R e ottimizzazione dello storage di SharePoint. Puoi raggiungerlo a iqbal@alachisoft.com.