Autor: Iqbal Khan
Um cache distribuído permite melhorar muito o desempenho e a escalabilidade do aplicativo. O desempenho do aplicativo é aprimorado porque um cache na memória é muito mais rápido para acesso a dados do que um banco de dados. E a escalabilidade é alcançada aumentando o cache para vários servidores como um cache distribuído e ganhando não apenas mais capacidade de armazenamento, mas também mais transações por segundo.
Apesar desses benefícios poderosos, há um problema enfrentado por muitos caches na memória. E isso tem a ver com o fato de que a maioria dos dados é relacional, enquanto um cache geralmente é uma simples tabela de hash com um conceito de par chave-valor. Cada item é armazenado no cache de forma independente, sem qualquer conhecimento de outros itens relacionados. E isso dificulta que os aplicativos acompanhem os relacionamentos entre os diferentes itens armazenados em cache, tanto para buscá-los quanto para a integridade dos dados, caso um item seja atualizado ou removido e seus itens relacionados também sejam atualizados ou removidos do banco de dados. Quando isso acontece, o cache não sabe disso e não pode lidar com isso.
Um aplicativo típico da vida real lida com dados relacionais que têm relacionamentos um para um, muitos para um, um para muitos e muitos para muitos com outros elementos de dados no banco de dados. Isso requer que a integridade referencial seja mantida em diferentes elementos de dados relacionados. Portanto, para preservar a integridade dos dados no cache, o cache deve entender esses relacionamentos e manter a mesma integridade referencial.
Para lidar com essas situações, a dependência de cache foi introduzida pela Microsoft no ASP.NET Cache. A dependência de cache permite que você relacione vários elementos em cache e, sempre que você atualizar ou remover qualquer item em cache, o cache removerá automaticamente todos os itens em cache relacionados para garantir a integridade dos dados. Então, quando seu aplicativo não encontrar esses itens relacionados no cache na próxima vez que precisar deles, o aplicativo vai para o banco de dados e busca a cópia mais recente desses itens e os armazena em cache novamente com a integridade referencial correta mantida.
Esse é um ótimo recurso no ASP.NET Cache, mas o ASP.NET Cache é, por design, um cache autônomo que é bom apenas para ambientes em processo de servidor único. Mas, para escalabilidade, você deve usar um cache distribuído que pode viver fora do seu processo de aplicativo e pode ser dimensionado para vários servidores de cache. NCache é um cache e, felizmente, fornece o mesmo recurso de dependência de cache em um ambiente distribuído. Você pode ter itens armazenados em cache em um servidor de cache físico dependendo dos itens armazenados em cache em outro servidor de cache físico, desde que ambos sejam partes do mesmo cache clusterizado lógico. E, NCache cuida de todos os problemas de integridade de dados mencionados acima.
Este artigo explica como você pode usar a dependência de cache para lidar com relacionamentos um para um, um para muitos e muitos para muitos no cache. Ele usa NCache como exemplo, mas os mesmos conceitos se aplicam ao ASP.NET Cache.
Apesar, NCache fornece vários tipos de dependências, incluindo Dependência de dados, Dependência de arquivo, Dependência SQL e Dependência personalizada, este artigo discute apenas a dependência de dados para lidar com relacionamentos entre itens armazenados em cache.
A Dependência de Dados é um recurso que permite especificar que um item armazenado em cache depende de outro item armazenado em cache. Então, se o segundo item armazenado em cache for atualizado ou removido, o primeiro item que dependia dele também será removido do cache. A dependência de dados permite especificar dependências de vários níveis em que A depende de B, que depende de C. Então, se C for atualizado ou removido, A e B serão removidos do cache.
Abaixo está um breve exemplo de como usar a dependência de dados para especificar a dependência multinível.
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;
}
}
O exemplo a seguir é usado neste artigo para demonstrar como vários tipos de relacionamentos são tratados no cache.
No diagrama acima, as seguintes relações são mostradas:
Para os relacionamentos acima, os seguintes objetos de domínio são projetados.
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 você pode ver, as classes Cliente e Produto contêm um _Lista de pedidos para conter uma lista de todos os objetos Order relacionados a este cliente. Da mesma forma, a classe Order contém _Cliente e _Produtos membros de dados para apontar para o objeto Customer ou Product relacionado. Agora, é o trabalho do código de persistência que está carregando esses objetos do banco de dados para garantir que sempre que um Customer for carregado, todos os seus objetos Order também sejam carregados.
Abaixo, mostrarei como cada um desses relacionamentos é tratado no cache.
Sempre que você buscou um objeto do cache que também tem um relacionamento um-para-um ou muitos-para-um com outro objeto, seu código de persistência também pode ter carregado o objeto relacionado. No entanto, nem sempre é necessário carregar o objeto relacionado porque o aplicativo pode não precisar dele naquele momento. Se o seu código de persistência carregou o objeto relacionado, você precisa manipulá-lo.
Existem duas maneiras de lidar com isso. Chamarei uma de otimista e a outra de pessimista e explicarei cada uma delas a seguir:
Abaixo está o código-fonte para lidar com a situação otimista. Observe que tanto o objeto primário quanto seus objetos relacionados são armazenados em cache como um item porque a serialização do objeto primário também inclui os 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();
}
Abaixo está o código-fonte para lidar com a situação pessimista, pois o cenário otimista não requer o uso de Dependência de Dados.
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();
}
O código acima carrega um objeto Order do banco de dados e os objetos Customer e Product são carregados automaticamente com ele porque o objeto Order tem um relacionamento de muitos para um com eles. O aplicativo adiciona os objetos Cliente e Produto ao cache e, em seguida, adiciona o objeto Pedido ao cache, mas com uma dependência dos objetos Cliente e Produto. Dessa forma, se algum desses objetos Cliente ou Produto for atualizado ou removido do cache, o objeto Pedido será automaticamente removido do cache para preservar a integridade dos dados. O aplicativo não precisa acompanhar esse relacionamento.
Sempre que você buscar um objeto do cache que também tenha um relacionamento um-para-muitos com outro objeto, seu código de persistência pode carregar o objeto primário e uma coleção de todos os seus objetos relacionados um-para-muitos. No entanto, nem sempre é necessário carregar os objetos relacionados porque o aplicativo pode não precisar deles no momento. Se o seu código de persistência carregou os objetos relacionados, você precisa manipulá-los no cache. Observe que os objetos relacionados são todos mantidos em uma coleção e isso apresenta problemas próprios que são discutidos abaixo.
Existem três maneiras de lidar com isso. Vou chamar um otimista, um levemente pessimista e um realmente pessimista e explicarei cada um deles abaixo:
Abaixo está um exemplo de como você pode lidar com relacionamentos um-para-muitos de forma otimista. Observe que a coleção que contém os objetos relacionados é serializada como parte do objeto principal ao ser colocada no 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();
}
Abaixo está um exemplo de como lidar com o relacionamento um-para-muitos de maneira levemente pessimista.
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();
}
No exemplo acima, a lista de objetos Order relacionados a este Experiência e dinâmica de loja é armazenado em cache separadamente. A coleção inteira é armazenada em cache como um item porque estamos assumindo que ninguém modificará diretamente os objetos Order individuais separadamente. O aplicativo sempre irá buscá-lo por meio deste Cliente e modificará e armazenará novamente em cache toda a coleção.
Outro caso é o tratamento pessimista de relacionamentos um-para-muitos, que é semelhante ao modo como lidamos com coleções no cache. Esse tópico é discutido na próxima seção.
Há muitas situações em que você busca uma coleção de objetos do banco de dados. Isso pode ser devido a uma consulta que você executou ou pode ser um relacionamento um para muitos retornando uma coleção de objetos relacionados no lado "muitos". De qualquer forma, o que você obtém é uma coleção de objetos que devem ser tratados no cache adequadamente.
Existem duas maneiras de lidar com coleções, conforme explicado abaixo:
Abaixo está um exemplo de como lidar com coleções de forma otimista.
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();
}
No exemplo acima, toda a coleção é armazenada em cache como um item e todos os objetos Customer mantidos dentro da coleção são serializados automaticamente junto com a coleção e o cache. Portanto, não há necessidade de criar nenhuma dependência de dados aqui.
Abaixo está um exemplo de como lidar com coleções de forma pessimista.
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();
}
No exemplo acima, cada objeto da coleção é armazenado em cache como um item separado e, em seguida, toda a coleção é armazenada em cache, bem como um item. A coleção tem uma dependência de dados em todos os seus objetos que são armazenados em cache separadamente. Dessa forma, se algum desses objetos for atualizado ou removido, a coleção também será removida do cache.
Autor: Iqbal Khan trabalha para Alachisoft , uma empresa de software líder que fornece soluções de cache distribuído .NET e Java, mapeamento O/R e otimização de armazenamento do SharePoint. Você pode alcançá-lo em iqbal@alachisoft.com.