Gestione delle relazioni di dati nella cache distribuita

Autore: Iqbal Khan

Introduzione

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.

Che cos'è la dipendenza dai dati 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;
    }
}

Dipendenza dai dati a più livelli


Relazioni tra dati

L'esempio seguente viene utilizzato in questo articolo per dimostrare come vengono gestiti i vari tipi di relazioni nella cache.

Gestione delle relazioni di dati
Figura 2: relazioni nel database

Nel diagramma sopra, sono mostrate le seguenti relazioni:

  • Uno a molti: Esistono due di queste relazioni e sono:
    1. Cliente su ordinazione
    2. Prodotto su ordinazione
  • Molti a Uno: Esistono due di queste relazioni e sono:
    1. Ordine al cliente
    2. Ordine al prodotto
  • Molti a molti: Esiste una di queste relazioni ed è:
    1. Da cliente a prodotto (tramite Ordine)

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.

Gestire le relazioni uno-a-uno/molti-a-uno

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:

  1. Gestione ottimistica delle relazioni: In questo, assumiamo che anche se ci sono relazioni, nessun altro modificherà l'oggetto correlato separatamente. Chi vuole modificare gli oggetti correlati lo recupererà tramite l'oggetto primario nella cache e sarà quindi in grado di modificare sia gli oggetti primari che quelli correlati. In questo caso, non è necessario memorizzare entrambi questi oggetti separatamente nella cache. Pertanto, l'oggetto primario contiene l'oggetto correlato ed entrambi vengono archiviati come un elemento memorizzato nella cache nella cache. E tra di loro non viene creata alcuna dipendenza dai dati.
  2. Gestione pessimistica delle relazioni: In questo caso, si presuppone che l'oggetto correlato possa essere recuperato e aggiornato in modo indipendente da un altro utente e pertanto l'oggetto correlato deve essere archiviato come elemento separato nella cache. Quindi, se qualcuno aggiorna o rimuove l'oggetto correlato, desideri che anche il tuo oggetto principale venga rimosso dalla cache. In questo caso, creerai una Data Dependency tra i due oggetti.

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();
}

Gestione ottimistica della relazione molti-a-uno

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();
}

Gestione pessimistica delle relazioni molti-a-uno

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.

Gestire le relazioni uno-a-molti

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:

  1. Gestione ottimistica delle relazioni: In questo, assumiamo che anche se ci sono relazioni, nessun altro modificherà gli oggetti correlati separatamente. Chi vuole modificare gli oggetti correlati li recupererà tramite l'oggetto primario nella cache e sarà quindi in grado di modificare sia gli oggetti primari che quelli correlati. In questo caso, non è necessario memorizzare entrambi questi oggetti separatamente nella cache. Pertanto, l'oggetto primario contiene l'oggetto correlato ed entrambi vengono archiviati come un elemento memorizzato nella cache nella cache. E tra di loro non viene creata alcuna dipendenza dai dati.
  2. Gestione moderatamente pessimistica delle relazioni: In questo caso, si presume che gli oggetti correlati possano essere recuperati indipendentemente ma solo come l'intera collezione e mai come singoli oggetti. Pertanto, si archivia la raccolta come un elemento memorizzato nella cache e si crea una dipendenza dalla raccolta all'oggetto principale. Quindi, se qualcuno aggiorna o rimuove l'oggetto principale, vuoi che anche la tua raccolta venga rimossa dalla cache.
  3. Gestione davvero pessimistica delle relazioni: In questo caso, si presume che tutti gli oggetti nella raccolta correlata possano anche essere recuperati individualmente dall'applicazione e modificati. Pertanto, è necessario non solo archiviare la raccolta ma anche tutti i loro singoli oggetti nella cache separatamente. Tieni presente, tuttavia, che ciò potrebbe causare problemi di prestazioni perché stai effettuando più viaggi nella cache che potrebbe risiedere attraverso la rete su un server cache. Ne parlerò nella prossima sezione che si occupa di "Gestione delle raccolte nella cache".

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();
}

Gestire il rapporto uno-a-molti con ottimismo


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();
}

Gestire la relazione uno-a-molti in modo leggermente pessimistico

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.

Gestione delle raccolte nella cache

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:

  1. Gestione ottimistica delle raccolte: In questo, assumiamo che l'intera raccolta debba essere memorizzata nella cache come un unico elemento perché nessuno preleverà e modificherà individualmente gli oggetti conservati all'interno della raccolta. La raccolta potrebbe essere memorizzata nella cache per un breve periodo di tempo e questa ipotesi potrebbe essere molto valida.
  2. Gestione pessimistica delle raccolte: In questo caso, assumiamo che i singoli oggetti all'interno della collezione possano essere recuperati separatamente e modificati. Pertanto, inseriamo nella cache l'intera raccolta, ma poi inseriamo nella cache anche ogni singolo oggetto e creiamo una dipendenza dalla raccolta ai singoli oggetti.

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();
}

Gestire le raccolte con ottimismo

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();
}

Gestire le raccolte in modo pessimistico

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.

© Copyright Alachisoft 2002 - . Tutti i diritti riservati. NCache è un marchio registrato di Diyatech Corp.