Autor: Iqbal Kan
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é.
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;
}
}
El siguiente ejemplo se utiliza en este artículo para demostrar cómo se manejan varios tipos de relaciones en la memoria caché.
En el diagrama anterior, se muestran las siguientes relaciones:
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é.
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:
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();
}
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();
}
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.
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:
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();
}
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();
}
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.
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:
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();
}
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();
}
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.