Gestión de relaciones de datos en caché distribuida

Autor: Iqbal Kan

Introducción

Una memoria caché distribuida le permite mejorar en gran medida el rendimiento y la escalabilidad de las aplicaciones. El rendimiento de la aplicación mejora porque una caché en memoria es mucho más rápida para acceder a los datos que una base de datos. Y, la escalabilidad se logra aumentando el caché a múltiples servidores como un caché distribuido y ganando no solo más capacidad de almacenamiento sino también más transacciones por segundo.

A pesar de estos poderosos beneficios, hay un problema al que se enfrentan muchas cachés en memoria. Y eso tiene que ver con el hecho de que la mayoría de los datos son relacionales, mientras que un caché suele ser una simple tabla hash con un concepto de par clave-valor. Cada elemento se almacena en el caché de forma independiente sin ningún conocimiento de ningún otro elemento relacionado. Y esto dificulta que las aplicaciones realicen un seguimiento de las relaciones entre los diferentes elementos almacenados en caché tanto para recuperarlos como para la integridad de los datos en caso de que un elemento se actualice o elimine y sus elementos relacionados también se actualicen o eliminen en la base de datos. Cuando esto sucede, el caché no lo sabe y no puede manejarlo.

Una aplicación típica de la vida real trata con datos relacionales que tienen relaciones uno a uno, muchos a uno, uno a muchos y muchos a muchos con otros elementos de datos en la base de datos. Esto requiere que se mantenga la integridad referencial en diferentes elementos de datos relacionados. Por lo tanto, para poder preservar la integridad de los datos en el caché, la memoria caché debe comprender estas relaciones y mantener la misma integridad referencial.

Para manejar estas situaciones, Microsoft introdujo Cache Dependency en ASP.NET Cache. La dependencia de caché le permite relacionar varios elementos almacenados en caché y luego, cada vez que actualice o elimine cualquier elemento almacenado en caché, el caché elimina automáticamente todos sus elementos almacenados en caché relacionados para garantizar la integridad de los datos. Luego, cuando su aplicación no encuentre estos elementos relacionados en el caché la próxima vez que los necesite, la aplicación va a la base de datos y obtiene la copia más reciente de estos elementos, y luego los vuelve a almacenar en caché manteniendo la integridad referencial correcta.

Esta es una gran característica en ASP.NET Cache, pero ASP.NET Cache es, por diseño, una caché independiente que es buena solo para entornos en proceso de un solo servidor. Pero, para la escalabilidad, debe utilizar un caché distribuida que puede vivir fuera de su proceso de aplicación y puede escalar a múltiples servidores de caché. NCache es un caché de este tipo y, afortunadamente, proporciona la misma función de dependencia de caché en un entorno distribuido. Puede tener elementos almacenados en caché en un servidor de caché físico según los elementos almacenados en caché en otro servidor de caché físico, siempre que ambos sean partes de la misma caché lógica en clúster. Y, NCache se ocupa de todos los problemas de integridad de datos mencionados anteriormente.

Este artículo explica cómo puede usar la dependencia de caché para manejar relaciones uno a uno, uno a muchos y muchos a muchos en el caché. Usa NCache como ejemplo, pero los mismos conceptos se aplican a ASP.NET Cache.

A pesar de que, NCache proporciona varios tipos de dependencias, incluyendo Dependencia de datos, Dependencia de archivo, Dependencia SQLy Dependencia personalizada, este artículo solo analiza la dependencia de datos para manejar las relaciones entre los elementos almacenados en caché.

¿Qué es la dependencia de datos en caché?

Dependencia de datos es una función que le permite especificar que un elemento almacenado en caché depende de otro elemento almacenado en caché. Luego, si el segundo elemento almacenado en caché alguna vez se actualiza o elimina, el primer elemento que dependía de él también se elimina del caché. Dependencia de datos le permite especificar dependencias de varios niveles donde A depende de B, que luego depende de C. Luego, si C se actualiza o elimina, tanto A como B se eliminan de la memoria caché.

A continuación se muestra un breve ejemplo de cómo utilizar la dependencia de datos para especificar la dependencia de varios niveles.

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

Dependencia de datos de varios niveles


Relaciones de datos

El siguiente ejemplo se utiliza en este artículo para demostrar cómo se manejan varios tipos de relaciones en la memoria caché.

Gestión de relaciones de datos
Figura 2: Relaciones en la base de datos

En el diagrama anterior, se muestran las siguientes relaciones:

  • Uno a muchos: Hay dos relaciones de este tipo y son:
    1. Cliente a pedido
    2. Producto a pedido
  • Muchos a uno: Hay dos relaciones de este tipo y son:
    1. Pedido al cliente
    2. Pedido a producto
  • Muchos a muchos: Hay una de esas relaciones y es:
    1. Cliente a producto (vía pedido)

Para las relaciones anteriores, se diseñan los siguientes objetos de 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;
    }

Como puede ver, las clases Cliente y Producto contienen un _Lista de orden para contener una lista de todos los objetos de pedido que están relacionados con este cliente. De manera similar, la clase Order contiene _Cliente y _Producto miembros de datos para apuntar al objeto Cliente o Producto relacionado. Ahora, el trabajo del código de persistencia es cargar estos objetos desde la base de datos para garantizar que cada vez que se cargue un Cliente, también se carguen todos sus objetos Pedido.

A continuación, mostraré cómo se maneja cada una de estas relaciones en el caché.

Manejo de relaciones uno a uno/muchos a uno

Siempre que haya obtenido un objeto del caché que también tenga una relación de uno a uno o de muchos a uno con otro objeto, es posible que su código de persistencia también haya cargado el objeto relacionado. Sin embargo, no siempre es necesario cargar el objeto relacionado porque es posible que la aplicación no lo necesite en ese momento. Si su código de persistencia ha cargado el objeto relacionado, entonces debe manejarlo.

Hay dos maneras de manejar esto. Llamaré a una forma optimista y a la otra pesimista y explicaré cada una de ellas a continuación:

  1. Manejo optimista de las relaciones: En esto, asumimos que aunque existan relaciones, nadie más va a modificar el objeto relacionado por separado. Cualquiera que desee modificar los objetos relacionados lo buscará a través del objeto primario en el caché y, por lo tanto, estará en condiciones de modificar tanto los objetos primarios como los relacionados. En este caso, no tenemos que almacenar ambos objetos por separado en el caché. Por lo tanto, el objeto principal contiene el objeto relacionado y ambos se almacenan como un elemento en caché en el caché. Y no se crea ninguna dependencia de datos entre ellos.
  2. Manejo pesimista de las relaciones: En este caso, asume que otro usuario puede obtener y actualizar el objeto relacionado de forma independiente y, por lo tanto, el objeto relacionado debe almacenarse como un elemento en caché separado. Luego, si alguien actualiza o elimina el objeto relacionado, desea que su objeto principal también se elimine del caché. En este caso, creará una Dependencia de datos entre los dos objetos.

A continuación se muestra el código fuente para manejar la situación optimista. Tenga en cuenta que tanto el objeto principal como sus dos objetos relacionados se almacenan en caché como un elemento porque la serialización del objeto principal también incluiría los objetos relacionados.

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

Manejo optimista de la relación muchos a uno

A continuación se muestra el código fuente para manejar la situación pesimista, ya que el escenario optimista no requiere ningún uso de Dependencia de datos.

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

Manejo pesimista de las relaciones de muchos a uno.

El código anterior carga un objeto Pedido de la base de datos y los objetos Cliente y Producto se cargan automáticamente con él porque el objeto Pedido tiene una relación de muchos a uno con ellos. Luego, la aplicación agrega los objetos Cliente y Producto al caché y luego agrega el objeto Pedido al caché, pero con una dependencia de los objetos Cliente y Producto. De esta forma, si alguno de estos objetos Cliente o Producto se actualiza o elimina de la memoria caché, el objeto Pedido se elimina automáticamente de la memoria caché para preservar la integridad de los datos. La aplicación no tiene que realizar un seguimiento de esta relación.

Manejo de relaciones de uno a muchos

Siempre que haya obtenido un objeto de la memoria caché que también tenga una relación de uno a muchos con otro objeto, su código de persistencia puede cargar tanto el objeto principal como una colección de todos sus objetos relacionados de uno a muchos. Sin embargo, no siempre es necesario cargar los objetos relacionados porque es posible que la aplicación no los necesite en este momento. Si su código de persistencia ha cargado los objetos relacionados, debe manejarlos en el caché. Tenga en cuenta que todos los objetos relacionados se mantienen en una colección y esto presenta problemas propios que se analizan a continuación.

Hay tres maneras de manejar esto. Llamaré a una forma optimista, una levemente pesimista y una realmente pesimista y explicaré cada una de ellas a continuación:

  1. Manejo optimista de las relaciones: En esto, asumimos que aunque existan relaciones, nadie más va a modificar los objetos relacionados por separado. Cualquiera que desee modificar los objetos relacionados, los buscará a través del objeto primario en el caché y, por lo tanto, estará en condiciones de modificar tanto los objetos primarios como los relacionados. En este caso, no tenemos que almacenar ambos objetos por separado en el caché. Por lo tanto, el objeto principal contiene el objeto relacionado y ambos se almacenan como un elemento en caché en el caché. Y no se crea ninguna dependencia de datos entre ellos.
  2. Manejo levemente pesimista de las relaciones: En este caso, asume que los objetos relacionados se pueden obtener de forma independiente, pero solo como la colección completa y nunca como objetos individuales. Por lo tanto, almacena la colección como un elemento en caché y crea una dependencia de la colección al objeto principal. Luego, si alguien actualiza o elimina el objeto principal, desea que su colección también se elimine del caché.
  3. Manejo realmente pesimista de las relaciones: En este caso, asume que todos los objetos en la colección relacionada también pueden ser obtenidos individualmente por la aplicación y modificados. Por lo tanto, no solo debe almacenar la colección sino también todos sus objetos individuales en el caché por separado. Sin embargo, tenga en cuenta que es probable que esto cause problemas de rendimiento porque está realizando varios viajes al caché que puede residir en la red en un servidor de caché. Discutiré esto en la siguiente sección que trata sobre "Manejo de colecciones en caché".

A continuación se muestra un ejemplo de cómo puede manejar las relaciones de uno a muchos con optimismo. Tenga en cuenta que la colección que contiene los objetos relacionados se serializa como parte del objeto principal cuando se coloca en el caché.

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

Manejar la relación de uno a muchos con optimismo


A continuación se muestra un ejemplo de cómo manejar una relación de uno a muchos de manera ligeramente pesimista.

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

Manejar la relación de uno a muchos de manera levemente pesimista

En el ejemplo anterior, la lista de objetos Order que están relacionados con este Local se almacena en caché por separado. La colección completa se almacena en caché como un elemento porque asumimos que nadie modificará directamente los objetos Order individuales por separado. La aplicación siempre lo buscará a través de este Cliente y modificará y volverá a almacenar en caché toda la colección nuevamente.

Otro caso es el manejo pesimista de las relaciones de uno a muchos, que es similar a cómo manejamos las colecciones en el caché. Ese tema se analiza en la siguiente sección.

Manejo de colecciones en la caché

Hay muchas situaciones en las que obtiene una colección de objetos de la base de datos. Esto podría deberse a una consulta que ejecutó o podría ser una relación de uno a muchos que devuelve una colección de objetos relacionados en el lado "varios". De cualquier manera, lo que obtienes es una colección de objetos que deben manejarse en el caché de manera adecuada.

Hay dos formas de manejar las colecciones como se explica a continuación:

  1. Manejo optimista de las colecciones: En esto, asumimos que toda la colección debe almacenarse en caché como un elemento porque nadie buscará ni modificará individualmente los objetos guardados dentro de la colección. La colección puede almacenarse en caché durante un breve período de tiempo y esta suposición puede ser muy válida.
  2. Manejo pesimista de las colecciones: En este caso, asumimos que los objetos individuales dentro de la colección pueden obtenerse por separado y modificarse. Por lo tanto, almacenamos en caché toda la colección, pero también almacenamos en caché cada objeto individual y creamos una dependencia de la colección a los objetos individuales.

A continuación se muestra un ejemplo de cómo manejar las colecciones de manera optimista.

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

Manejar las colecciones con optimismo

En el ejemplo anterior, toda la colección se almacena en caché como un elemento y todos los objetos Cliente que se mantienen dentro de la colección se serializan automáticamente junto con la colección y el caché. Por lo tanto, no es necesario crear ninguna dependencia de datos aquí.

A continuación se muestra un ejemplo de cómo manejar las colecciones de manera pesimista.

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

Manejar las colecciones de manera pesimista

En el ejemplo anterior, cada objeto de la colección se almacena en caché como un elemento separado y luego se almacena en caché toda la colección, así como un elemento. La colección tiene una dependencia de datos en todos sus objetos que se almacenan en caché por separado. De esta forma, si alguno de estos objetos se actualiza o elimina, la colección también se elimina de la memoria caché.


Escrito por: Iqbal Khan trabaja para Alachisoft , una empresa de software líder que ofrece soluciones de almacenamiento en caché distribuido .NET y Java, mapeo O/R y optimización de almacenamiento de SharePoint. Puedes localizarlo en iqbal@alachisoft.com.

© Copyright Alachisoft 2002 - Todos los derechos reservados. NCache es una marca registrada de Diyatech Corp.