Gerenciando relacionamentos de dados em cache distribuído

Autor: Iqbal Khan

Introdução

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.

O que é dependência de dados 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;
    }
}

Dependência de dados em vários níveis


Relacionamentos de dados

O exemplo a seguir é usado neste artigo para demonstrar como vários tipos de relacionamentos são tratados no cache.

Gerenciando relacionamentos de dados
Figura 2: Relacionamentos no Banco de Dados

No diagrama acima, as seguintes relações são mostradas:

  • Um para muitos: Existem duas dessas relações e são elas:
    1. Cliente a encomendar
    2. Produto a encomendar
  • Muitos para um: Existem duas dessas relações e são elas:
    1. Encomenda ao Cliente
    2. Encomendar ao Produto
  • Muitos para muitos: Existe uma tal relação e que é:
    1. Cliente para Produto (via Pedido)

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.

Como lidar com relacionamentos um-para-um/muitos-para-um

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:

  1. Manejo otimista dos relacionamentos: Neste, assumimos que, embora existam relacionamentos, ninguém mais irá modificar o objeto relacionado separadamente. Quem quiser modificar os objetos relacionados irá buscá-lo através do objeto primário no cache e, portanto, estará em condições de modificar os objetos primários e relacionados. Nesse caso, não precisamos armazenar esses dois objetos separadamente no cache. Portanto, o objeto primário contém o objeto relacionado e ambos são armazenados como um item em cache no cache. E nenhuma dependência de dados é criada entre eles.
  2. Manejo pessimista dos relacionamentos: Nesse caso, você assume que o objeto relacionado pode ser buscado e atualizado independentemente por outro usuário e, portanto, o objeto relacionado deve ser armazenado como um item em cache separado. Então, se alguém atualizar ou remover o objeto relacionado, você deseja que seu objeto principal também seja removido do cache. Nesse caso, você criará uma dependência de dados entre os dois objetos.

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

Manuseio otimista do relacionamento muitos-para-um

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

Manejo pessimista de relacionamentos muitos-para-um

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.

Lidando com relacionamentos um-para-muitos

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:

  1. Manejo otimista dos relacionamentos: Neste, assumimos que, embora existam relacionamentos, ninguém mais modificará os objetos relacionados separadamente. Quem quiser modificar os objetos relacionados irá buscá-los através do objeto primário no cache e, portanto, estará em condições de modificar os objetos primários e relacionados. Nesse caso, não precisamos armazenar esses dois objetos separadamente no cache. Portanto, o objeto primário contém o objeto relacionado e ambos são armazenados como um item em cache no cache. E nenhuma dependência de dados é criada entre eles.
  2. Manejo levemente pessimista dos relacionamentos: Nesse caso, você assume que os objetos relacionados podem ser buscados independentemente, mas apenas como a coleção inteira e nunca como objetos individuais. Portanto, você armazena a coleção como um item em cache e cria uma dependência da coleção para o objeto primário. Então, se alguém atualizar ou remover o objeto primário, você deseja que sua coleção também seja removida do cache.
  3. Tratamento realmente pessimista dos relacionamentos: Nesse caso, você assume que todos os objetos na coleção relacionada também podem ser buscados individualmente pelo aplicativo e modificados. Portanto, você deve não apenas armazenar a coleção, mas também todos os seus objetos individuais no cache separadamente. Observe, no entanto, que isso provavelmente causaria problemas de desempenho porque você está fazendo várias viagens ao cache, que pode estar residindo na rede em um servidor de cache. Discutirei isso na próxima seção que trata de "Manipulação de coleções em cache".

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

Lidando com o relacionamento um-para-muitos com otimismo


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

Lidar com relacionamento um-para-muitos levemente pessimista

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.

Manipulando Coleções no Cache

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:

  1. Manipulação otimista de coleções: Neste, assumimos que toda a coleção deve ser armazenada em cache como um item porque ninguém irá buscar e modificar individualmente os objetos mantidos dentro da coleção. A coleção pode ser armazenada em cache por um breve período de tempo e essa suposição pode ser muito válida.
  2. Manuseio pessimista de cobranças: Nesse caso, assumimos que objetos individuais dentro da coleção podem ser buscados separadamente e modificados. Portanto, armazenamos em cache a coleção inteira, mas também armazenamos em cache cada objeto individual e criamos uma dependência da coleção para os objetos individuais.

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

Lidando com coleções de forma otimista

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

Lidar com coleções de forma pessimista

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.

© Copyright Alachisoft 2002 - . Todos os direitos reservados. NCache é uma marca registrada da Diyatech Corp.